/*
 * Copyright (c) Des Herriott 1993, 1994
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation, and that the name of the copyright holder not be used in
 * advertising or publicity pertaining to distribution of the software without
 * specific, written prior permission.  The copyright holder makes no
 * representations about the suitability of this software for any purpose.  It
 * is provided "as is" without express or implied warranty.
 *
 * THE COPYRIGHT HOLDER DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
 * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
 * EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY SPECIAL, INDIRECT OR
 * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
 * DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
 * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 * PERFORMANCE OF THIS SOFTWARE.
 *
 * Author: Martin Smith (msmith@lssec.bt.co.uk)
 */

/*
 * An emulation of the ZX INTERFACE 1 MICRODRIVES
 *
 * By Martin H. Smith
 *
 * This code has nothing to do with my employers, they certainly don't pay
 * me to enjoy myself like this! If only!
 *
 * Saturday 12th March 1994
 *
 * Note: This emulation has been produced from my own knowledge and guesses
 * at how the Interface 1 hardware works based on a full disassembly I
 * jointly wrote 10 years or so ago. This means I cannot vouch that the port
 * operations are emulated exactly as any other Spectrum microdrive emulator
 * does but the .MDR files should be compatible with other emulators.
 */

#ifdef ZX_IF1

#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <malloc.h>

#include "z80.h"
#include "resource.h"
#include "if1.h"

extern void Undocumented(void);
extern void ret(void);
extern FILE *open_xzx_file(char *name, char *access);

static long mdrive_bytes_out = 0L;		/* statistics */
static long mdrive_bytes_in = 0L;

static int gap_clock = 0;				/* counter for GAP bit */
static int gap_bit = 0;					/* state of GAP bit */

static uns8 shadow[SHADOW_ROM_SIZE];	/* new rom image */
static uns8 romcpy[SHADOW_ROM_SIZE];	/* saved old rom image */


/* data on each drive */

static mdrive_data drives[MAX_DRIVES + 1];

static long drive_seq = 0L;				/* cache sequence number */

static int pagedin = 0;					/* new ROM paged in? */
static int if1_active = 0;				/* IF1 emulation active? */
static int preamble = 0;				/* preamble skip byte counter */

static int drive_running = 0;			/* number of current drive */
static int drive_count = 0;				/* drive count for start / stop */

/* do we want to translate carriage returns and newlines on IO? */
static int translate_crlf = 0;

/* forward */ static void stop_drive(int);

/*
 * Page in the new 8K rom, i.e. block copy its image into the low 8K of
 * spectrum memory
 */

void page_in_rom(void)

{
	if (if1_active && !pagedin)					  /* we can page in */
	{
		memcpy(theMemory, shadow, SHADOW_ROM_SIZE);
		pagedin = 1;
	}
}


/*
 * Page out the new rom, put back the lower half of the speccy 16K ROM
 */

void page_out_rom(void)

{
	if (if1_active && pagedin)					  /* we can page out */
	{
		memcpy(theMemory, romcpy, SHADOW_ROM_SIZE);
		pagedin = 0;
	}
}


/*
 * Put down our breakpoints, if the appropriate rom has been loaded
 * into memory
 */

static void set_breakpoints(void)

{
	if (shadow[RS232_IP_ADDRESS] == RS232_IP_BPT_BYTE &&
		shadow[RS232_OP_ADDRESS] == RS232_OP_BPT_BYTE)
	{
		shadow[RS232_IP_ADDRESS] = 0xed;
		shadow[RS232_IP_ADDRESS + 1] = RS232_IP_BPT;
		
		shadow[RS232_OP_ADDRESS] = 0xed;
		shadow[RS232_OP_ADDRESS + 1] = RS232_OP_BPT;
	}
	else
	{
		fprintf(stderr,
				"xzx: unknown IF1 rom, RS232 to stdio is disabled\n");
#ifdef DEBUG
		fprintf(stderr, "xzx: bytes %x %x\n", 
				shadow[RS232_IP_ADDRESS],
				shadow[RS232_OP_ADDRESS]);
#endif
	}
}



/*
 * Ensure that a cartridge file is a normal file and has the correct
 * size
 */

static int test_cartridge_file(int drive)

{
	struct stat sbuff;
	int res = 0;						/* bad file */

	if (!fstat(drives[drive].fd, &sbuff)) /* stat ok */
	{
		if (S_ISREG(sbuff.st_mode) && sbuff.st_size == CART_FILE_SIZE)
		{
			res = 1;					/* good file size */
		}
		else
			fprintf(stderr, "xzx: Cart file %s in drive %d is bad\n",
					drives[drive].filename, drive);
	}

	return res;
}


/*
 * Try and load the cartridge files into the drives. Set up the initial
 * arrays and handle write protection
 */

static void setup_cartridge_files(char cname[8][256])

{
	int f;

	for (f = 1; f <= MAX_DRIVES; ++f)
	{
		drives[f].sequence = 0L;		          /* cache sequence */
		drives[f].protect = 1;					  /* protected */
		drives[f].tapepos = 0L;					  /* at start */
		drives[f].loaded = 0;					  /* no cartridge */
		drives[f].filename = cname[f - 1];		  /* cart file */
		drives[f].buffer = NULL;
		drives[f].fd = -1;

		if (cname[f - 1][0])			/* not null filename */
		{
			drives[f].fd = open(cname[f - 1], O_RDONLY);
			
			if (drives[f].fd > 0)
			{
				if (test_cartridge_file(f))
					drives[f].loaded = 1;	/* a valid file */
				
				close(drives[f].fd);
				drives[f].fd = -1;
			}
		}
	}
}


/*
 * Set up our interface emulation. If the user wants and IF1 and we manage
 * to load our ROM image file then activate the emulation and load the
 * cartridge files.
 */

void init_if1(struct config * cp)

{
	FILE *fp;
	
	if ((fp = open_xzx_file(cp->if1_rom, "r")) == NULL)
		fprintf(stderr, "xzx: can't open interface 1 ROM image\n");

	else if (cp -> if1_active) /* do we want to use the interface 1 ? */
	{
		if (fread(shadow, SHADOW_ROM_SIZE, 1, fp) != 1)
			fprintf(stderr, "xzx: can't open interface 1 rom file\n");
		else
		{
#ifdef DEBUG
			fprintf(stderr, "xzx: interface 1 rom file read ok.\n");
#endif
			if1_active = 1;

			set_breakpoints();
			
			setup_cartridge_files(cp -> cartfiles);
		}

		fclose(fp);

		translate_crlf = cp->translate_nl;
	}

	
	memcpy(romcpy, theMemory, SHADOW_ROM_SIZE);	  /* get copy of old rom */
}


/*
 * Load up a cartridge file
 */

static int load_cartridge_file(int d)

{
	int res = 0;
	drives[d].fd = open(drives[d].filename, O_RDONLY);

	drives[d].tapepos /= (long) CART_FILE_SECTOR_SIZE;
	drives[d].tapepos *= (long) CART_FILE_SECTOR_SIZE;
	drives[d].loaded = 0;
	drives[d].dirty = 0;
	drives[d].protect = 0;
	
	if (drives[d].fd > 0)
	{
		drives[d].buffer = (uns8 *) malloc(CART_FILE_SIZE);

		if (drives[d].buffer &&
                    read(drives[d].fd, drives[d].buffer, CART_FILE_SIZE)
			== CART_FILE_SIZE)					  /* got file ok */
		{
			drives[d].loaded = 1;

			/* the flag may say writeable ... */

			drives[d].protect = drives[d].buffer[CART_FILE_SIZE - 1] ? 1 : 0;

			/* but the file might be read only */

			if (access(drives[d].filename, W_OK) != 0)
				drives[d].protect = 1;
#ifdef DEBUG
			fprintf(stderr, "xzx: read %s cart file %d [%s]\n",
				drives[d].protect ? "R/O" : "R/W",
				d, drives[d].filename);
#endif

			res = 1;
		}
		else
		{
			free(drives[d].buffer);
			drives[d].buffer = NULL;
			fprintf(stderr, "xzx: can't read cartridge file %s\n",
					drives[d].filename);
		}

		close(drives[d].fd);
		drives[d].fd = -1;
	}
	
	return res;
}


/*
 * Get rid of a cartridge file, writing it back if necessary
 */

static void unload_cartridge_file(int d)

{
	if (drives[d].dirty && !drives[d].protect)
	{
		drives[d].fd = open(drives[d].filename, O_RDWR);

		if (drives[d].fd > 0)					  /* file is open */
		{
			if (write(drives[d].fd, drives[d].buffer, CART_FILE_SIZE)
				!= CART_FILE_SIZE)
			{
				fprintf(stderr, "xzx: failed to write cart file %s\n",
						drives[d].filename);
			}
			else
			{
#ifdef DEBUG
				fprintf(stderr, "xzx: wrote cart file %d [%s]\n",
				d, drives[d].filename);
#endif
			}

			close(drives[d].fd);
			drives[d].fd = -1;
		}
		else
		{
			fprintf(stderr, "xzx: failed to open cart file %s for write\n",
					drives[d].filename);
		}
	}
#ifdef IF1_MINOR_DEBUG
	else
	{
		fprintf(stderr, "xzx: unloaded cartridge %d\n", d);
	}
#endif

	drives[d].dirty = 0;
	drives[d].loaded = 0;
}


/*
 * We are allowed to keep up to a certain number of drive images in memory.
 * if we are at that number then free up the drive which was used the
 * longest time ago
 */

static void flush_cache(void)

{
	int count = 0;
	int picked = 0;
	long sequence = 0L;
	int f;

	for (f = 1; f <= MAX_DRIVES; ++f)
	{
		if (drives[f].buffer)			/* this drive in RAM */
		{
			++count;

			/* find the oldest drive */
			
			if (sequence == 0 || drives[f].sequence < sequence)
			{
				sequence = drives[f].sequence;
				picked = f;
			}
		}
	}

	if (picked && count >= KEEP_DRIVES_IN_RAM) /* need to flush */
	{
		free(drives[picked].buffer);
		drives[picked].buffer = 0;

#ifdef DEBUG
		fprintf(stderr, "xzx: threw drive %d out of RAM\n", picked);
#endif
	}
}


/*
 * Start the 'motor' of a microdrive
 */

static void start_drive(int which)

{
#ifdef IF1_MINOR_DEBUG
	fprintf(stderr, "xzx: drive %d on.\n", which);
#endif

	gap_clock = 0;
	gap_bit = 0;

	drives[which].tapepos /= (long) CART_FILE_SECTOR_SIZE;
	drives[which].tapepos *= (long) CART_FILE_SECTOR_SIZE;

	if (drive_running && drive_running != which) /* turn this one off */
	{
		stop_drive(drive_running);
		drive_running = 0;
	}
		
	if (drive_running != which)				  /* this one not going */
	{
		if (drives[which].buffer)
		{
			drive_running = which;		      /* already in memory */
			drives[drive_running].sequence = ++drive_seq;
		}
		else
		{
			flush_cache();				      /* clear out cache */
			
			if (load_cartridge_file(which))
			{
				drives[drive_running].sequence = ++drive_seq;
				drive_running = which;
			}
#ifdef DEBUG
			else
				fprintf(stderr, "xzx: but no cartridge.\n");
#endif
		}
	}
}


/*
 * Stop a microdrive motor
 */

static void stop_drive(int d)

{
#ifdef IF1_MINOR_DEBUG
		fprintf(stderr, "xzx: drive %d off.\n", d);
#endif

		/* see if we are stopping the current one */

		if (d == drive_running)
		{
			unload_cartridge_file(d);
			drive_running = 0;
		}
}


/*
 * Output a byte to the control port of the microdrives (0xef). Note this
 * is not a perfect emulation, in particular I have not emulated the
 * behaviour of outing 0 erasing cartridges!
 */

void out_port_ef(uns8 byte)

{
	static int last_byte = 0;

	if (drive_count > 7 || drive_count < 0)
		drive_count = 0;				/* insanity check on drive count */

	/* if we're about to start writing account for 12 bytes which must be
	   skipped (the preamble) before the actual data */
	
	if (byte == 0xe2 && !preamble)
	{
		preamble = 12;	
	}

	/* step the count forwards, turn things on and off */
	
	if (byte == 0xee && last_byte == 0xee)		  /* reset count */
	{
		drive_count = 0;
		byte = 0;
#ifdef DEBUG
		/* fprintf(stderr, "xzx: interface 1 drive_count reset.\n"); */
#endif
	}
	else if (byte == 0xec && last_byte == 0xee)	  /* turn on drive */
	{
		++drive_count;
		start_drive(9 - drive_count);
	}
	else if (byte == 0xed && last_byte == 0xef)	  /* turn off drive */
	{
		++drive_count;
		stop_drive(9 - drive_count);
	}

	last_byte = byte;
}


/*
 * Send a byte to the microdrive data port. The byte is thrown away if a
 * drive is not running
 */

void out_port_e7(uns8 byte)

{
	++mdrive_bytes_out;							  /* for stats freaks! */

	if (drive_running && !preamble)		/* a byte to write out */
	{
		/* find offset in current sector */
		
		int pos = drives[drive_running].tapepos % CART_FILE_SECTOR_SIZE;

		/* if we lose write sync it is very serious! This is detected by
		   writing a sector header with a reset flag bit or record header with
		   a set flag bit. If this happens we mark the cartridge as write
		   protected. This terminates the operation with an error, without
		   writing back the duff cart file */

		if ((!pos && (byte & 1) != 1) || (pos == 15 && (byte & 1) != 0))
		{
			fprintf(stderr, "xzx: microdrive write sync lost %d\n",
				drives[drive_running].tapepos);

			drives[drive_running].protect = 1; /* don't write the corruption */
		}

		/* store the byte away */
		
		drives[drive_running].buffer[drives[drive_running].tapepos] = 
			byte;

		drives[drive_running].dirty = 1; /* cartridge written to */

		/* 3 troublesome bytes are written after the ends of headers and
		   sectors. Lucky I remembered this one! */
		
		if (!preamble && (pos == (CART_FILE_SECTOR_SIZE - 1) || pos == 14))
			preamble = 3;

		/* has the 'tape' wrapped round ? */
		
		if (++(drives[drive_running].tapepos) == CART_FILE_SIZE - 1)
		{
			drives[drive_running].tapepos = 0L;
#ifdef DEBUG
			fprintf(stderr, "xzx: drive %d revolution (write)\n",
				drive_running);
#endif
		}
	}
	else if (drive_running && preamble > 0)
	{
		--preamble;						/* ignore a preamble byte */
	}
}


/*
 * Read in from the microdrive data port, if the drive is running we return
 * the next byte of the tape loop. If not we don't emulate the IF1 behaviour
 * of locking up. I hope no one minds.
 *
 * Note there is no read sync lost check as for write as unformatted carts
 * may be placed in drives.
 */

uns8 in_port_e7(void)

{
	int res = 0xff;

	if (drive_running)
	{
		++mdrive_bytes_in;

		res = drives[drive_running].buffer[drives[drive_running].tapepos];
		
		if (++drives[drive_running].tapepos == CART_FILE_SIZE - 1)
		{
			drives[drive_running].tapepos = 0L;
#ifdef DEBUG
			fprintf(stderr, "xzx: drive %d revolution (read)\n",
				drive_running);
#endif
		}
	}

	return res;
}


/*
 * Read a byte of data from the microdrive control port. Does not do
 * an exact emulation, just good enough to work and does not deal with
 * bits not related to the microdrive.
 *
 * If a drive is not running the status bits remain high and the interface
 * ROM will time us out with a Microdrive not present error.
 */

uns8 in_port_ef(void)

{
	uns8 res = 0xe0;					/* basic result */

	if (drive_running)
	{
		/* get notional position within a sector */
		
		int pos = drives[drive_running].tapepos % CART_FILE_SECTOR_SIZE;

		int sync_b = SYNC_BIT;

		if (pos == 0 || pos == 15)		/* sync at end of preamble */
		{
			sync_b = 0; /* sync bit reset, ready to start I/O */
		}
		else
		{
			/* feed the tape past the head until we reach a point where
			   a sync signal should occur */
			
			if ((++drives[drive_running].tapepos) == CART_FILE_SIZE - 1)
			{
#ifdef DEBUG
				fprintf(stderr, "xzx: drive %d revolution (sync)\n",
						drive_running);
#endif
				drives[drive_running].tapepos = 0L;
			}
		}

		if (!drives[drive_running].protect)	/* set up prot bit */
			res |= WRITE_PROT_BIT;

		/* just do the GAP bit as a simple clock, at least for now */

		if ((++gap_clock % GAP_CLOCK_RATE) == 0)
			gap_bit = !gap_bit;

		if (gap_bit)
			res |= GAP_BIT;

		res |= sync_b; 
	}
	else
		res |= GAP_BIT | SYNC_BIT | WRITE_PROT_BIT;	/* all high */

	return res;
}


#ifdef DEBUG
/*
 * Tell the user how many bytes were written
 */
static void report_mdrive_stats(void)

{
	fprintf(stderr, "xzx: %d bytes out to microdrive, %d bytes in\n",
 			mdrive_bytes_out, mdrive_bytes_in);
}
#endif


/*
 * Close down the IF1, all drives should be stopped at this point anyway
 * but it writes back cart files in case the emulator was stopped part
 * way through an operation by a quit / reset etc.
 */

void close_if1(void)

{
	int f;

	if (if1_active)
	{
		for (f = 1; f <= MAX_DRIVES; ++f)
		{
			if (drives[f].loaded && drives[f].buffer)
				unload_cartridge_file(f);
		}
	
#ifdef DEBUG
		report_mdrive_stats();
#endif
	}
}


/*
 * RS232 Input, A register is set to next byte from stdin.
 */

void rs232_ip_trap()

{

	if (!pagedin || *pc >= SHADOW_ROM_SIZE)	/* reserved instruction */
		Undocumented();
	else
	{
		*a = getchar();
		if (translate_crlf && *a == 10)
			*a = 13;
		ret();
	}
}


/*
 * RS232 Output, Byte in A register is sent to stdout.
 */

void rs232_op_trap()

{
	if (!pagedin || *pc >= SHADOW_ROM_SIZE) /* reserved instruction */
		Undocumented();
	else
	{
		if (translate_crlf && *a == 13)
			putchar(10);
		else
			putchar(*a);
		fflush(stdout);
		ret();
	}
}

#endif
