/* RAID5 driver for reconfiguration */

#include "raidreconf.h"
#include "rrc_common.h"

/* private contextual data for this driver */
typedef struct {
	int		algorithm;		/* which parity computation */
	int		raid_disks;		/* # disks in RAID	    */
	int		data_disks;		/* # data disks in RAID     */
	int		chunk_size;		/* bytes/chunk		    */
	rrc_disk_t	*disks;			/* disk info array	    */
	int		*disk_id_idx;		/* direct lookup of disk_id */
	unsigned long	tot_chunks;		/* chunks in this RAID	    */
	unsigned long	cur_block;		/* current block	    */
	unsigned long	tot_blocks;		/* blocks to copy	    */
	unsigned long	blocks_per_chunk;	/* should always be 1	    */
	unsigned long	*disk_total_blocks;	/* # blocks for each disk   */
} raid5_driver_private_t;

/* function prototypes */

static void *emalloc (int size);
static unsigned long raid5_compute_sector (unsigned long, unsigned int *, 
    unsigned int *, raid5_driver_private_t *);
static unsigned long compute_blocknr (raid5_driver_private_t *, int, 
    unsigned long);

/* initialize this driver */

static const char *
raid5_initialize (void *context, md_cfg_entry_t *cfg, rrc_disk_t *cfgdisks,
    unsigned long *blocks)
{
	int d;
	unsigned long max_disk_id = 0;
	raid5_driver_private_t *ctx = (raid5_driver_private_t *) context;

	if (cfg->array.param.level != 5)
		return ("Wrong level for RAID-5 Driver");

	if (cfg->array.param.nr_disks < 3)
		return "Too few disks for RAID5";

	ctx->raid_disks = cfg->array.param.nr_disks;
	ctx->data_disks = ctx->raid_disks-1;
	ctx->chunk_size = cfg->array.param.chunk_size;
	ctx->algorithm = cfg->array.param.layout;
	ctx->cur_block = 0;
	ctx->disks = cfgdisks;

	/* sum the total blocks in the array */
	ctx->tot_chunks = 0;
	for (d = 0; d < ctx->raid_disks; ++d)
		ctx->tot_chunks += ctx->disks[d].chunks;

	/* RAID5 uses one disk's worth of data for parity, so subtract it */
	ctx->tot_chunks -= ctx->disks[0].chunks;

	/* set up the disk_id -> disks[] mapping */
	for (d = 0; d < ctx->raid_disks; ++d)
		if (ctx->disks[d].disk_id > max_disk_id)
			max_disk_id = ctx->disks[d].disk_id;

	if (max_disk_id > 0) {
		ctx->disk_id_idx = emalloc (sizeof (int) * (max_disk_id + 1));
		for (d = 0; d < ctx->raid_disks; ++d)
			ctx->disk_id_idx[ctx->disks[d].disk_id] = d;
	}
	
	ctx->blocks_per_chunk = (cfg->array.param.chunk_size / MD_BLK_SIZ) / 
	    reconf_block_size;
	
	*blocks = ctx->blocks_per_chunk * ctx->tot_chunks;

	/* we only want to actually copy the MIN blocks of the arrays */
	if (source_blocks && sink_blocks)
		ctx->tot_blocks = MIN (source_blocks, sink_blocks);
	else
		ctx->tot_blocks = *blocks;

	ctx->disk_total_blocks = emalloc (sizeof (unsigned long) * 
	    ctx->raid_disks);

	for (d = 0; d < ctx->raid_disks; ++d)
		ctx->disk_total_blocks[d] = 
		    cfgdisks[d].chunks * (ctx->chunk_size / MD_BLK_SIZ) / 
		    reconf_block_size;

	return NULL;
}

/* fill the wish_list with wishes for blocks we wish to write */

static driver_status_t
raid5_request_blocks (void *context)
{
	raid5_driver_private_t *ctx = (raid5_driver_private_t *) context;

	while (ctx->cur_block < ctx->tot_blocks) {
		if (!can_wish_again ())
			return LDR_INCOMPLETE;

		insert_wish (ctx->cur_block);
		++ctx->cur_block;
	}

	return LDR_DONE;
}

/* update the super-block, tell the kernel about any new disks, and
** start the array
*/

static const char *
raid5_update_super (void *context)
{
	unsigned long d;
	int mdfile;
	int rc;
	mdu_param_t mdpar;

	printf ("Updating superblocks...\n");
	if (analyze_sb (&ver, mkraid, new_md_cfg, 1, 0, 1)) {
		fprintf (stderr, "Error analyzing superblock.\n");
		return "RAID-5 Superblock analysis error";
	}

	/* Tell the kernel about this */
	mdfile = open (new_md_cfg->md_name, O_RDONLY);
	if (mdfile < 0) {
		static char grump[1024];
		snprintf (grump, sizeof (grump), "can't open %s (%s).", 
		    new_md_cfg->md_name, strerror (errno));
		fprintf (stderr, "%s\n", grump);
		return (grump);
	}

	/* turn off the clean state bit, 
	** so the parity blocks will be recalculated
	** "Unclean!  Unclean!"
	*/
	new_md_cfg->array.param.state &= ~(1 << MD_SB_CLEAN);
	rc = ioctl (mdfile, SET_ARRAY_INFO, 
	    (unsigned long) &new_md_cfg->array.param);
	if (rc) {
		fprintf (stderr, "Failed setting array info for device %s\n", 
		    new_md_cfg->md_name);
		return "RAID-5 Superblock info error";
	}	
	printf ("Array is updated with kernel.\n");

	for (d = 0; d != new_md_cfg->array.param.nr_disks; d++) {
		rc = ioctl (mdfile, ADD_NEW_DISK,
		    (unsigned long) (new_md_cfg->array.disks + d));
		if (rc) {
			static char grump[1024];
			snprintf (grump, sizeof (grump), 
			    "Failed adding disk %lu to array.", d);
			fprintf (stderr, "%s\n", grump);
			return (grump);
		}
	}
	close (mdfile);

	printf ("Disks re-inserted in array... "
	    "Hold on while starting the array...\n");

	mdfile = open (new_md_cfg->md_name, O_RDWR);
	if (mdfile < 0) {
		static char grump[1024];
		snprintf (grump, sizeof (grump), "can't open %s (%s).", 
		    new_md_cfg->md_name, strerror (errno));
		fprintf (stderr, "%s\n", grump);
		return (grump);
	}

	/* Now run the array ! */
	memset (&mdpar, 0, sizeof (mdpar));
	mdpar.personality = RAID5;
	mdpar.chunk_size = new_md_cfg->array.param.chunk_size;
	rc = ioctl (mdfile, RUN_ARRAY, (unsigned long) &mdpar);
	close (mdfile);
	if (rc) {
		switch (errno) {
		case EBUSY:
			printf ("Array %s is already running\n", 
			    new_md_cfg->md_name);
			break;
		default:
			perror(new_md_cfg->md_name);
		}
		return "RAID-5 Superblock update error";
	}

	return 0;
}

/* the following func is lifted (and edited) from the kernel sources */

/*
 * Input: a 'big' sector number,
 * Output: index of the data and parity disk, and the sector # in them.
 */
static unsigned long
raid5_compute_sector (unsigned long r_sector, 
    unsigned int *dd_idx,
    unsigned int *pd_idx, 
    raid5_driver_private_t *ctx)
{
	unsigned long stripe;
	unsigned long chunk_number;
	unsigned int chunk_offset;

# ifdef TESTING
# define PU(V)	printf ("%s = %lu ", #V, V)
# else
# define PU(V)
# endif /* TESTING */

	/* First compute the information on this sector */

	/*
	 * Compute the chunk number and the sector offset inside the chunk
	 */
	chunk_number = r_sector / ctx->blocks_per_chunk;
	chunk_offset = r_sector % ctx->blocks_per_chunk;

	PU (r_sector);
	PU (chunk_number);
	PU (chunk_offset);

	/*
	 * Compute the stripe number
	 */
	stripe = chunk_number / ctx->data_disks;
	PU (stripe);

	/*
	 * Compute the data disk and parity disk indexes inside the stripe
	 */
	*dd_idx = chunk_number % ctx->data_disks;

	/*
	 * Select the parity disk based on the user selected algorithm.
	 */
/***********************************************************
	if (ctx->level == 4)
		*pd_idx = ctx->data_disks;
	else 
***********************************************************/
	switch (ctx->algorithm) {
	case RAID5_ALGORITHM_LEFT_ASYMMETRIC:
		*pd_idx = ctx->data_disks - stripe % ctx->raid_disks;
		if (*dd_idx >= *pd_idx)
			(*dd_idx)++;
		break;
	case RAID5_ALGORITHM_RIGHT_ASYMMETRIC:
		*pd_idx = stripe % ctx->raid_disks;
		if (*dd_idx >= *pd_idx)
			(*dd_idx)++;
		break;
	case RAID5_ALGORITHM_LEFT_SYMMETRIC:
		*pd_idx = ctx->data_disks - stripe % ctx->raid_disks;
		*dd_idx = (*pd_idx + 1 + *dd_idx) % ctx->raid_disks;
		break;
	case RAID5_ALGORITHM_RIGHT_SYMMETRIC:
		*pd_idx = stripe % ctx->raid_disks;
		*dd_idx = (*pd_idx + 1 + *dd_idx) % ctx->raid_disks;
		break;
	default:
		fprintf (stderr, "raid5: unsupported algorithm %d\n", 
		    ctx->algorithm);
	}
	PU (*dd_idx);
	PU (*pd_idx);

	/*
	 * Finally, compute the new sector number
	 */

# ifdef TESTING
	printf ("old sector # = %lu, new sector # = %lu\n", r_sector,
	    stripe * blocks_per_chunk + chunk_offset);
# endif

	return stripe * ctx->blocks_per_chunk + chunk_offset;
}

static const char *
raid5_map_global_to_local (void *context, unsigned long gblock, int *diskid,
    unsigned long *lblock)
{
	raid5_driver_private_t *ctx = (raid5_driver_private_t *) context;
	int disk_idx, parity_id;

# define ROUTINE	"raid5_map_global_to_local"

	*lblock = raid5_compute_sector (gblock, &disk_idx, &parity_id, ctx);

	if (*lblock >= ctx->disk_total_blocks[disk_idx]) {
		fprintf (stderr, 
		    "\n%s: disk %d block out of range: %lu (%lu) gblock = %lu\n",
		    ROUTINE, disk_idx, *lblock, 
		    ctx->disk_total_blocks[disk_idx], gblock);
		abort ();
	}

	*diskid = ctx->disks[disk_idx].disk_id;

	return (NULL);

# undef ROUTINE

}


/* the following func is lifted from the kernel sources also */

static unsigned long
compute_blocknr (raid5_driver_private_t *ctx, int i, unsigned long dblock)
{
	int raid_disks = ctx->raid_disks, data_disks = raid_disks - 1;
	unsigned long new_sector = dblock;
	unsigned long stripe = new_sector / ctx->blocks_per_chunk;
	int chunk_offset = new_sector % ctx->blocks_per_chunk;
	int chunk_number, pd_idx;
	unsigned long r_sector, blocknr;

	/* compute the parity disk index "pd_idx" */
	pd_idx = stripe % raid_disks;
	if (ctx->algorithm == RAID5_ALGORITHM_LEFT_ASYMMETRIC ||
	    ctx->algorithm == RAID5_ALGORITHM_LEFT_SYMMETRIC)
		pd_idx = data_disks - pd_idx;

	if (pd_idx == i)	/* parity block, flag it */
		return (ULONG_MAX);

	/* adjust the data disk index "i" */
	switch (ctx->algorithm) {
	case RAID5_ALGORITHM_LEFT_ASYMMETRIC:
	case RAID5_ALGORITHM_RIGHT_ASYMMETRIC:
		if (i > pd_idx)
			i--;
		break;
	case RAID5_ALGORITHM_LEFT_SYMMETRIC:
	case RAID5_ALGORITHM_RIGHT_SYMMETRIC:
		if (i < pd_idx)
			i += raid_disks;
		i -= (pd_idx + 1);
		break;
	default:
		fprintf (stderr, "raid5: unsupported algorithm %d\n", 
		    ctx->algorithm);
	}

	/* calculate the new sector & block number */
	chunk_number = stripe * data_disks + i;
	r_sector = chunk_number * ctx->blocks_per_chunk + chunk_offset;
	blocknr = r_sector;

	return blocknr;
}

/* return the global block number from a sink disk id and disk-block */

static unsigned long
raid5_map_local_to_global (void *context, int disk_idx, unsigned long dblock)
{
	raid5_driver_private_t *ctx = (raid5_driver_private_t *) context;
	int disk_id = ctx->disks[ctx->disk_id_idx[disk_idx]].disk_id;

	return compute_blocknr (ctx, disk_id, dblock);
}

/* for each block above gblock, mark as free */

static void
raid5_free_blocks_above_gblock (void *context, unsigned long gblock)
{
	raid5_driver_private_t *ctx = (raid5_driver_private_t *) context;
	unsigned long block, dblock;
	unsigned long tot_blocks = ctx->tot_chunks * ctx->blocks_per_chunk;

	for (block = gblock; block < tot_blocks; ++block) {
		int diskid;
		raid5_map_global_to_local (ctx, block, &diskid, &dblock);
		unchecked_mark_disk_block_free (diskid, dblock);
	}
}

static void
raid5_unfree_all_blocks (void *context)
{
	raid5_driver_private_t *ctx = (raid5_driver_private_t *) context;
	int disk;

	for (disk = 0; disk < ctx->raid_disks; ++disk) {
		unsigned long block;

		for (block = 0; block < ctx->disk_total_blocks[disk]; ++block)
			mark_disk_block_unfree (ctx->disks[disk].disk_id, 
			    block);
	}
}

level_driver_t *
new_raid5_driver (void)
{
	level_driver_t *drv = malloc (sizeof (level_driver_t));
	raid5_driver_private_t *ctx = malloc (sizeof (raid5_driver_private_t));

	if (!drv || !ctx)
		return (0);
	
	drv->initialize = raid5_initialize;
	drv->request_blocks = raid5_request_blocks;
	drv->update_super = raid5_update_super;
	drv->map_global_to_local = raid5_map_global_to_local;
	drv->map_local_to_global = raid5_map_local_to_global;
	drv->free_blocks_above_gblock = raid5_free_blocks_above_gblock;
	drv->unfree_all_blocks = raid5_unfree_all_blocks;

	drv->priv = ctx;
	ctx->algorithm = -1;
	ctx->raid_disks = 0;
	ctx->data_disks = 0;
	ctx->chunk_size = 0;
	ctx->disks = NULL;
	ctx->tot_chunks = 0;
	ctx->blocks_per_chunk = 0;
	ctx->disk_total_blocks = NULL;

	return drv;
}

/* die on error from malloc */

static void *
emalloc (int size)
{
	void *ret;

	ret = malloc ((size_t) size);
	if (ret == NULL) {
		fprintf (stderr, "Out of memory: abort ()\n");
		abort ();
	}

	return (ret);
}
