/*
 * 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 XZX_IF1

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

#include "z80.h"
#include "mem.h"
#include "rtcfg.h"
#include "util.h"
#include "if1.h"

/* Where RS232 input can come from */
enum { FromTty, FromFile, FromPipe };

static int stdin_filetype, stdout_filetype;

extern int errno;

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 */

/* data on each drive */

static mdrive_data drives[MAX_DRIVES + 1];

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

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 */


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


/*
 * Put down our breakpoints, if the appropriate rom has been loaded
 * into memory
 */
static void set_breakpoints(void)
{
	int cur_page;

	cur_page = RPAGE(0);

	page_in(0, ROM2);

	if (read_byte(RS232_IP_ADDRESS) == RS232_IP_BPT_BYTE &&
		read_byte(RS232_OP_ADDRESS) == RS232_OP_BPT_BYTE)
	{
		/* write_byte() won't work here! */
		realMem[ROM2][RS232_IP_ADDRESS] = 0xed;
		realMem[ROM2][RS232_IP_ADDRESS + 1] = RS232_IP_BPT;
		realMem[ROM2][RS232_OP_ADDRESS] = 0xed;
		realMem[ROM2][RS232_OP_ADDRESS + 1] = RS232_OP_BPT;
	}
	else
	{
		xzx_mesg(XZX_WARN,
				"unknown IF1 rom, RS232 to stdio is disabled");
#ifdef DEBUG
		xzx_mesg(XZX_INFO, "bytes %x %x", 
				read_byte(RS232_IP_ADDRESS),
				read_byte(RS232_OP_ADDRESS));
#endif
	}

	page_in(0, ROM0);
}


/* Make file descriptor <fd> either blocking or non-blocking dependng on mode */
static void
set_blocking(int fd, int mode)
{
	int status, ostat;

#ifndef __GO32__
	
	status = fcntl(fd, F_GETFL, &ostat);
#ifdef O_NONBLOCK
	if (mode) ostat |= O_NONBLOCK; else ostat &= ~O_NONBLOCK;
#else
	if (mode) ostat |= O_NDELAY; else ostat &= ~O_NDELAY;
#endif
	status = fcntl(fd, F_SETFL, ostat);

	if (status == -1) {
		xzx_mesg(XZX_ERR,
			"couldn't make fd %d %s", fd, mode ? "nonblocking" : "blocking");
		perror("  -> reason");
	}

#endif

}


/*
 * Decide whether file descriptor <fd> is a normal file, a tty, or a pipe.
 * This affects the semantics of reads and writes via RS232.
 */
static int
classify_descriptor(int fd)
{
	struct stat buf;

	if (isatty(fd))
		return FromTty;

	fstat(fd, &buf);

	if (S_ISREG(buf.st_mode))
		return FromFile;
	else if (S_ISFIFO(buf.st_mode))
		return FromPipe;
	else {
		xzx_mesg(XZX_WARN, "don't know where fd %d is coming from?", fd);
		return -1;
	}
}


/*
 * 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
			xzx_mesg(XZX_ERR, "cart file %s in drive %d is bad",
					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;
			}
		}
	}
}


/* Close down IF1 emulation cleanly */
static void
shutdown_if1(void)
{
#ifndef __GO32__
	set_blocking(STDIN_FILENO, 0);
	set_blocking(STDOUT_FILENO, 0);
#endif
}


/*
 * 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(void)
{
	if (!GetCfg(if1_active))
		return;
	
	set_breakpoints();

	setup_cartridge_files(GetCfg(cartfiles));

	set_blocking(STDIN_FILENO, 1);
	stdin_filetype = classify_descriptor(STDIN_FILENO);

	set_blocking(STDOUT_FILENO, 1);
	stdout_filetype = classify_descriptor(STDOUT_FILENO);

	OnQuit(shutdown_if1);

#ifdef DEBUG
	xzx_mesg(XZX_INFO, "IF1 initialised successfully");
#endif
}


/*
 * 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
			xzx_mesg(XZX_INFO, "xzx: read %s cart file %d [%s]",
				drives[d].protect ? "R/O" : "R/W",
				d, drives[d].filename);
#endif

			res = 1;
		}
		else
		{
			free(drives[d].buffer);
			drives[d].buffer = NULL;
			xzx_mesg(XZX_ERR, "can't read cartridge file %s",
					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)
			{
				xzx_mesg(XZX_ERR, "failed to write cart file %s",
						drives[d].filename);
			}
			else
			{
#ifdef DEBUG
				xzx_mesg(XZX_INFO, "wrote cart file %d [%s]",
				d, drives[d].filename);
#endif
			}

			close(drives[d].fd);
			drives[d].fd = -1;
		}
		else
		{
			xzx_mesg(XZX_ERR, "failed to open cart file %s for write",
					drives[d].filename);
		}
	}
#ifdef IF1_MINOR_DEBUG
	else
	{
		xzx_mesg(XZX_INFO, "unloaded cartridge %d", 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
		xzx_mesg(XZX_INFO, "threw drive %d out of RAM", picked);
#endif
	}
}


/*
 * Start the 'motor' of a microdrive
 */
static void
start_drive(int which)
{
#ifdef IF1_MINOR_DEBUG
	xzx_mesg(XZX_INFO, "drive %d on.", 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
				xzx_mesg(XZX_INFO, "but no cartridge.");
#endif
		}
	}
}


/*
 * Stop a microdrive motor
 */
static void
stop_drive(int d)
{
#ifdef IF1_MINOR_DEBUG
		xzx_mesg(XZX_INFO, "drive %d stopped.", 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))
		{
			xzx_mesg(XZX_ERR, "microdrive write sync lost %d",
				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
			xzx_mesg(XZX_INFO, "drive %d revolution (write)",
				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
			xzx_mesg(XZX_INFO, "drive %d revolution (read)", 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
				xzx_mesg(XZX_INFO, "drive %d revolution (sync)",
						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)
{
	xzx_mesg(XZX_INFO, "%d bytes out to microdrive, %d bytes in",
 			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()
{
	char buf[1];
	int read_ret;

	read_ret = read(fileno(stdin), (void *)buf, 1);
	if (read_ret != 1) {
		switch (stdin_filetype) {
			case FromPipe: case FromTty:
				if (errno == 0 || errno == EAGAIN) {
					Clr(carryFlag); Set(zeroFlag); /* no input ready */
				} else {
					xzx_mesg(XZX_ERR,
						"read error on stdin - got %d bytes", read_ret);
					perror("  -> reason");
				}
				break;
			case FromFile:
				if (read_ret == 0) {
					Clr(carryFlag); Clr(zeroFlag); /* EOF */
				} else {
					xzx_mesg(XZX_ERR,
						"read error on stdin - got %d bytes", read_ret);
					perror("  -> reason");
				}
				break;
			default:
				xzx_mesg(XZX_ERR, "don't know what I'm reading?");
				break;
		} /* switch */
	} else {
		*a = buf[0];	/* successful read */
	}
#ifdef DEBUG
	xzx_mesg(XZX_INFO, "read code %d from stdin", buf[0]);
#endif
	if (*a == 10 && GetCfg(translate_nl))
		*a = 13;
	ret();
}


/*
 * RS232 Output, Byte in A register is sent to stdout.
 */
void rs232_op_trap()
{
	char buf[1];

	if (*a == 13 && GetCfg(strip_nl))
		return; /* don't do anything */

	if (*a == 13 && GetCfg(translate_nl))
		*a = 10;

	buf[0] = *a;
	write(fileno(stdout), (void *)buf, 1);
#ifdef DEBUG
	xzx_mesg(XZX_INFO, "wrote code %d to stdout", buf[0]);
#endif
	fflush(stdout);
	ret();
}

#endif
