/*  Copyright 1994,1996-2003,2005,2007,2009 Alain Knaff.
 *  This file is part of mtools.
 *
 *  Mtools is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Mtools is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Mtools.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Io to an xdf disk
 *
 * written by:
 *
 * Alain L. Knaff
 * alain@knaff.lu
 *
 */


#include "sysincludes.h"
#ifdef OS_linux
#include "msdos.h"
#include "mtools.h"
#include "devices.h"
#include "xdf_io.h"

/* Algorithms can't be patented */

typedef struct sector_map {
	unsigned int head:1;
	unsigned int size:7;
} sector_map_t;


static struct {
  unsigned char track_size;
  unsigned int track0_size:7;
  unsigned int rootskip:1;
  unsigned char rate;
  sector_map_t map[9];
} xdf_table[]= {
  {
    19, 16, 0, 0,
    {	{0,3},	{0,6},	{1,2},	{0,2},	{1,6},	{1,3},	{0,0} }
  },
  {
    23, 19, 0, 0,
    {	{0,3},	{0,4},	{1,6},	{0,2},	{1,2},	{0,6},	{1,4},	{1,3},	{0,0} }
  },
  {
    46, 37, 1, 0x43,
    {	{0,3},	{0,4},	{0,5},	{0,7},	{1,3},	{1,4},	{1,5},	{1,7},	{0,0} }
  },
  {
    24, 20, 1, 0,
    {	{0,5},	{1,6},	{0,6},	{1, 5} }
  },
  {
    48, 41, 1, 0,
    {	{0,6},	{1,7},	{0,7},	{1, 6} }
  }
};

#define NUMBER(x) (sizeof(x)/sizeof(x[0]))

typedef struct {
	unsigned char begin; /* where it begins */
	unsigned char end;
	unsigned char sector;
	unsigned char sizecode;

	unsigned int dirty:1;
	unsigned int phantom:2;
	unsigned int valid:1;
	unsigned int head:1;
} TrackMap_t;



typedef struct Xdf_t {
	struct Stream_t head;

	int fd;
	char *buffer;

	bool track_valid;
	uint8_t current_track;

	sector_map_t *map;

	uint32_t track_size;
	int track0_size;
	uint16_t sector_size;
	uint8_t FatSize;
	uint16_t RootDirSize;
	TrackMap_t *track_map;

	unsigned char last_sector;
	unsigned char rate;

	unsigned int stretch:1;
	unsigned int rootskip:1;
	signed  int drive:4;

	const char *postcmd;
} Xdf_t;

typedef struct {
	unsigned char head;
	unsigned char sector;
	unsigned char ptr;
} Compactify_t;


static int analyze_reply(RawRequest_t *raw_cmd, int do_print)
{
	int ret, bytes, newbytes;

	bytes = 0;
	while(1) {
		ret = analyze_one_reply(raw_cmd, &newbytes, do_print);
		bytes += newbytes;
		switch(ret) {
			case 0:
				return bytes;
			case 1:
				raw_cmd++;
				break;
			case -1:
				if(bytes)
					return bytes;
				else
					return 0;
		}
	}
}



static int send_cmd(int fd, RawRequest_t *raw_cmd, int nr,
		    const char *message, int retries)
{
	int j;
	int ret=-1;

	if(!nr)
		return 0;
	for (j=0; j< retries; j++){
		switch(send_one_cmd(fd, raw_cmd, message)) {
			case -1:
				return -1;
			case 1:
				j++;
				continue;
			case 0:
				break;
		}
		if((ret=analyze_reply(raw_cmd, j)) > 0)
			return ret; /* ok */
	}
	if(j > 1 && j == retries) {
		fprintf(stderr,"Too many errors, giving up\n");
		return 0;
	}
	return -1;
}



#define REC (This->track_map[ptr])
#define END(x) (This->track_map[(x)].end)
#define BEGIN(x) (This->track_map[(x)].begin)

static int add_to_request(Xdf_t *This, unsigned char ptr,
			  RawRequest_t *request, int *nr,
			  int direction, Compactify_t *compactify)
{
#if 0
	if(direction == MT_WRITE) {
		printf("writing %d: %u %d %d %d [%02x]\n",
		       ptr, This->current_track,
		       REC.head, REC.sector, REC.sizecode,
		       *(This->buffer + ptr * This->sector_size));
	} else
			printf(" load %d.%u\n", This->current_track, ptr);
#endif
	if(REC.phantom) {
		if(direction== MT_READ)
			memset(This->buffer + ptr * This->sector_size, 0,
			       128u << REC.sizecode);
		return 0;
	}

	if(*nr &&
	   RR_SIZECODE(request+(*nr)-1) == REC.sizecode &&
	   compactify->head == REC.head &&
	   compactify->ptr + 1 == ptr &&
	   compactify->sector +1 == REC.sector) {
		RR_SETSIZECODE(request+(*nr)-1, REC.sizecode);
	} else {
		if(*nr)
			RR_SETCONT(request+(*nr)-1);
		RR_INIT(request+(*nr));
		RR_SETDRIVE(request+(*nr), This->drive);
		RR_SETRATE(request+(*nr), This->rate);
		RR_SETTRACK(request+(*nr), This->current_track);
		RR_SETPTRACK(request+(*nr),
			     This->current_track << This->stretch);
		RR_SETHEAD(request+(*nr), REC.head);
		RR_SETSECTOR(request+(*nr), REC.sector);
		RR_SETSIZECODE(request+(*nr), REC.sizecode);
		RR_SETDIRECTION(request+(*nr), direction);
		RR_SETDATA(request+(*nr),
			   This->buffer + ptr * This->sector_size);
		(*nr)++;
	}
	compactify->ptr = ptr;
	compactify->head = REC.head;
	compactify->sector = REC.sector;
	return 0;
}


static void add_to_request_if_invalid(Xdf_t *This, unsigned char ptr,
				     RawRequest_t *request, int *nr,
				     Compactify_t *compactify)
{
	if(!REC.valid)
		add_to_request(This, ptr, request, nr, MT_READ, compactify);

}


static void adjust_bounds(Xdf_t *This, uint32_t ibegin, uint32_t iend,
			  uint8_t *begin, uint8_t *end)
{
	/* translates begin and end from byte to sectors */
	*begin = (uint8_t) (ibegin / This->sector_size);
	*end = (uint8_t) ((iend + This->sector_size - 1) / This->sector_size);
}


static inline int try_flush_dirty(Xdf_t *This)
{
	unsigned char ptr;
	int nr, bytes;
	RawRequest_t requests[100];
	Compactify_t compactify;

	if(!This->track_valid)
		return 0;

	nr = 0;
	for(ptr=0; ptr < This->last_sector; ptr=REC.end)
		if(REC.dirty)
			add_to_request(This, ptr,
				       requests, &nr,
				       MT_WRITE, &compactify);
#if 1
	bytes = send_cmd(This->fd,requests, nr, "writing", 4);
	if(bytes < 0)
		return bytes;
#else
	bytes = 0xffffff;
#endif
	for(ptr=0; ptr < This->last_sector; ptr=REC.end)
		if(REC.dirty) {
			if(bytes >= REC.end - REC.begin) {
				bytes -= REC.end - REC.begin;
				REC.dirty = 0;
			} else
				return 1;
		}
	return 0;
}



static int flush_dirty(Xdf_t *This)
{
	int ret;

	while((ret = try_flush_dirty(This))) {
		if(ret < 0)
			return ret;
	}
	return 0;
}


static ssize_t load_data(Xdf_t *This, uint32_t ibegin, uint32_t iend,
			 int retries)
{
	unsigned char ptr;
	int nr, bytes;
	RawRequest_t requests[100];
	Compactify_t compactify;
	unsigned char begin, end;
	adjust_bounds(This, ibegin, iend, &begin, &end);

	ptr = begin;
	nr = 0;
	for(ptr=REC.begin; ptr < end ; ptr = REC.end)
		add_to_request_if_invalid(This, ptr, requests, &nr,
					  &compactify);
	bytes = send_cmd(This->fd,requests, nr, "reading", retries);
	if(bytes < 0)
		return bytes;
	ptr = begin;
	for(ptr=REC.begin; ptr < end ; ptr = REC.end) {
		if(!REC.valid) {
			if(bytes >= REC.end - REC.begin) {
				bytes -= REC.end - REC.begin;
				REC.valid = 1;
			} else if(ptr > begin)
				return ptr * This->sector_size;
			else
				return -1;
		}
	}
	return end * This->sector_size;
}

static void mark_dirty(Xdf_t *This, uint32_t ibegin, uint32_t iend)
{
	int ptr;
	unsigned char begin, end;

	adjust_bounds(This, ibegin, iend, &begin, &end);

	ptr = begin;
	for(ptr=REC.begin; ptr < end ; ptr = REC.end) {
		REC.valid = 1;
		if(!REC.phantom)
			REC.dirty = 1;
	}
}


static ssize_t load_bounds(Xdf_t *This, uint32_t begin, uint32_t end)
{
	unsigned char lbegin, lend;

	adjust_bounds(This, begin, end, &lbegin, &lend);

	if(begin != BEGIN(lbegin) * This->sector_size &&
	   end != BEGIN(lend) * This->sector_size &&
	   lend < END(END(lbegin)))
		/* contiguous end & begin, load them in one go */
		return load_data(This, begin, end, 4);

	if(begin != BEGIN(lbegin) * This->sector_size) {
		ssize_t ret = load_data(This, begin, begin, 4);
		if(ret < 0)
			return ret;
	}

	if(end != BEGIN(lend) * This->sector_size) {
		ssize_t ret = load_data(This, end, end, 4);
		if(ret < 0)
			return BEGIN(lend) * This->sector_size;
	}
	return lend * This->sector_size;
}

/* Fill out a map that is just sufficient to read boot sector */
static void fill_boot(Xdf_t *This)
{
	uint8_t ptr=0;

	REC.head = 0;
	REC.sector = 129;
	REC.phantom = 0;

	REC.begin = ptr;
	REC.end = ptr+1;

	REC.sizecode = 2;

	REC.valid = 0;
	REC.dirty = 0;
	This->last_sector=1;
	This->current_track=0;
}


static uint8_t fill_t0(Xdf_t *This, uint8_t ptr, unsigned int size,
		       uint8_t *sector, uint8_t *head)
{
	unsigned int n;

	for(n = 0; n < size; ptr++,n++) {
		REC.head = *head;
		REC.sector = *sector + 129;
		REC.phantom = 0;
		(*sector)++;
		if(!*head && *sector >= This->track0_size - 8) {
			*sector = 0;
			*head = 1;
		}
	}
	return ptr;
}


static uint8_t fill_phantoms(Xdf_t *This, uint8_t ptr, uint8_t size)
{
	unsigned int n;

	for(n = 0; n < size; ptr++,n++)
		REC.phantom = 1;
	return ptr;
}

static int decompose(Xdf_t *This, mt_off_t iwhere, size_t len,
		     uint32_t *begin, uint32_t *end, uint8_t boot)
{
	uint8_t ptr;
	sector_map_t *map;
	uint8_t lbegin, lend;
	uint32_t track_size = This->track_size * 1024;

	smt_off_t track = (smt_off_t) iwhere / track_size;
	uint32_t where = (uint32_t) ((smt_off_t) iwhere % track_size);

	*begin = where;
	if(where + len > track_size)
		*end = track_size;
	else
		*end = (uint32_t) (where + len);

	if(This->current_track == track && !boot)
		/* already OK, return immediately */
		return 0;
	if(!boot)
		flush_dirty(This);
	if(track >= 80)
		return -1;
	This->current_track = (uint8_t) track;
	This->track_valid = true;

	if(track) {
		for(ptr=0, map=This->map; map->size; map++) {
			/* iterate through all sectors */
			lbegin = ptr;
			lend = ptr +
				(uint8_t) ((128u<<map->size)/This->sector_size);
			for( ; ptr < lend ; ptr++) {
				REC.begin = lbegin;
				REC.end = lend;

				REC.head = map->head;
				REC.sector = map->size + 128;
				REC.sizecode = map->size;

				REC.valid = 0;
				REC.dirty = 0;
				REC.phantom = 0;
			}
		}
		REC.begin = REC.end = ptr;
	} else {
		uint8_t sector, head;

		head = 0;
		sector = 0;

		for(ptr=boot; ptr < 2 * This->track_size; ptr++) {
			REC.begin = ptr;
			REC.end = ptr+1;

			REC.sizecode = 2;

			REC.valid = 0;
			REC.dirty = 0;
		}

		/* boot & 1st fat */
		ptr=fill_t0(This, 0, 1 + This->FatSize, &sector, &head);

		/* second fat */
		ptr=fill_phantoms(This, ptr, This->FatSize);

		/* root dir */
		ptr=fill_t0(This, ptr, This->RootDirSize, &sector, &head);

		/* "bad sectors" at the beginning of the fs */
		ptr=fill_phantoms(This, ptr, 5);

		if(This->rootskip)
			sector++;

		/* beginning of the file system */
		ptr = fill_t0(This, ptr,
			      (This->track_size - This->FatSize) * 2 -
			      This->RootDirSize - 6,
			      &sector, &head);
	}
	This->last_sector = ptr;
	return 0;
}


static ssize_t xdf_pread(Stream_t *Stream, char *buf,
			 mt_off_t where, size_t len)
{
	uint32_t begin, end;
	ssize_t ret;
	DeclareThis(Xdf_t);

	if(decompose(This, truncBytes32(where), len, &begin, &end, 0) < 0)
		/* Read beyond end of device */
		return 0;
	ret = load_data(This, begin, end, 4);
	if(ret < 0 || (size_t) ret < begin)
		return -1;
	maximize(len, (size_t) ret - begin);
	memcpy(buf, This->buffer + begin, len);
	return (ssize_t) (end - begin);
}

static ssize_t xdf_pwrite(Stream_t *Stream, char *buf,
			  mt_off_t where, size_t len)
{
	uint32_t begin, end;
	ssize_t len2;
	DeclareThis(Xdf_t);

	if(decompose(This, truncBytes32(where), len, &begin, &end, 0) < 0) {
		/* Write beyond end of device */
		errno = EFBIG;
		return -1;
	}

	len2 = load_bounds(This, begin, end);
	if(len2 < 0)
		return -1;
	maximize(end, (uint32_t)len2);
	len2 -= begin;
	maximize(len, (size_t) len2);
	memcpy(This->buffer + begin, buf, len);
	mark_dirty(This, begin, end);
	return (ssize_t) (end - begin);
}

static int xdf_flush(Stream_t *Stream)
{
	DeclareThis(Xdf_t);

	return flush_dirty(This);
}

static int xdf_free(Stream_t *Stream)
{
	int ret;
	DeclareThis(Xdf_t);
	Free(This->track_map);
	Free(This->buffer);
	ret = close(This->fd);
	postcmd(This->postcmd);
	return ret;
}


static int check_geom(Xdf_t *This, struct device *dev)
{
	unsigned int sect;

	if (!IS_MFORMAT_ONLY(dev)) {
	    if(compare(dev->sectors, 19) &&
	       compare(dev->sectors, 23) &&
	       compare(dev->sectors, 24) &&
	       compare(dev->sectors, 46) &&
	       compare(dev->sectors, 48))
		return 1;

	    /* check against contradictory info from configuration file */
	    if(compare(dev->heads, 2))
		return 1;
	}

	/* check against info from boot */
	if(This) {
		sect = This->track_size;
		if((sect != 19 && sect != 23 && sect != 24 &&
		    sect != 46 && sect != 48) ||
		   (!IS_MFORMAT_ONLY(dev) && compare(dev->sectors, sect)))
			return 1;
	}
	return 0;
}

static void set_geom(Xdf_t *This, struct device *dev)
{
	/* fill in config info to be returned to user */
	dev->heads = 2;
	dev->use_2m = 0xff;
	dev->sectors = (uint16_t) This->track_size;
	dev->tracks = 80;
}

static int config_geom(Stream_t *Stream, struct device *dev,
		       struct device *orig_dev UNUSEDP)
{
	DeclareThis(Xdf_t);
	if(check_geom(This, dev))
		return 1;
	set_geom(This, dev);
	return 0;
}

static Class_t XdfClass = {
	0,
	0,
	xdf_pread,
	xdf_pwrite,
	xdf_flush,
	xdf_free,
	config_geom,
	0, /* get_data */
	0, /* pre-allocate */
	0, /* get_dosConvert */
	0 /* discard */
};

Stream_t *XdfOpen(struct device *dev, const char *name,
		  int mode, char *errmsg, struct xdf_info *info)
{
	Xdf_t *This;
	uint32_t begin, end;
	union bootsector *boot;
	unsigned int type;
	uint16_t fatSize;

	if(dev && ((!SHOULD_USE_XDF(dev) && !getenv("MTOOLS_USE_XDF")) ||
		   check_geom(NULL, dev)))
		return NULL;

	This = New(Xdf_t);
	if (!This)
		return NULL;
	init_head(&This->head, &XdfClass, NULL);

	This->sector_size = 512;
	This->stretch = 0;
	This->postcmd = 0;

	precmd(dev);
	if(dev)
		This->postcmd = dev->postcmd;

	This->fd = open(name,
			((mode | dev->mode) & ~O_ACCMODE) |
			O_EXCL | O_NDELAY | O_RDWR);
	if(This->fd < 0) {
#ifdef HAVE_SNPRINTF
		snprintf(errmsg,199,"xdf floppy: open: \"%s\"", strerror(errno));
#else
		sprintf(errmsg,"xdf floppy: open: \"%s\"", strerror(errno));
#endif
		goto exit_0;
	}
	closeExec(This->fd);
	
	This->drive = GET_DRIVE(This->fd);
	if(This->drive < 0)
		goto exit_1;

	/* allocate buffer */
	This->buffer = (char *) malloc(96 * 512);
	if (!This->buffer)
		goto exit_1;

	This->track_valid = false;
	This->track_map = (TrackMap_t *)
		calloc(96, sizeof(TrackMap_t));
	if(!This->track_map)
		goto exit_2;

	/* lock the device on writes */
	if (lock_dev(This->fd, mode == O_RDWR, dev)) {
#ifdef HAVE_SNPRINTF
		snprintf(errmsg,199,"xdf floppy: device \"%s\" busy:",
			dev->name);
#else
		sprintf(errmsg,"xdf floppy: device \"%s\" busy:",
			dev->name);
#endif
		goto exit_3;
	}

	/* Before reading the boot sector, assume dummy values suitable
	 * for reading just the boot sector */
	fill_boot(This);
	This->rate = 0;
	if (load_data(This, 0, 1, 4) < 0 ) {
		This->rate = 0x43;
		if(load_data(This, 0, 1, 4) < 0)
			goto exit_3;
	}

	boot = (union bootsector *) This->buffer;

	fatSize = BOOT_WORD(fatlen);
	if(fatSize > UINT8_MAX) {
		fprintf(stderr, "Fat size %d too large\n", fatSize);
		exit(1);
	}
	This->FatSize = (uint8_t) fatSize;
	This->RootDirSize = BOOT_WORD(dirents)/16;
	This->track_size = BOOT_WORD(nsect);
	for(type=0; type < NUMBER(xdf_table); type++) {
		if(xdf_table[type].track_size == This->track_size) {
			This->map = xdf_table[type].map;
			This->track0_size = xdf_table[type].track0_size;
			This->rootskip = xdf_table[type].rootskip;
			This->rate = xdf_table[type].rate;
			break;
		}
	}
	if(type == NUMBER(xdf_table))
		goto exit_3;

	if(info) {
		info->RootDirSize = This->RootDirSize;
		info->FatSize = This->FatSize;
		info->BadSectors = 5;
	}
	decompose(This, 0, 512, &begin, &end, 1);

	if(dev)
		set_geom(This, dev);
	return &This->head;

exit_3:
	Free(This->track_map);
exit_2:
	Free(This->buffer);
exit_1:
	close(This->fd);
exit_0:
	Free(This);
	return NULL;
}

#endif

/* Algorithms can't be patented */
