/*
 * Z80SIM  -  a Z80-CPU simulator
 *
 * Copyright (C) 1987-92 by Udo Munk
 *
 * This modul contains a complex I/O-simulation for the Z80-CPU
 * simulation. Because this is an example, what you can do with
 * the CPU-emulation, you may change this modul for your needs,
 * and use it, or parts of it, in your own I/O-simulations.
 *
 * History:
 * 28-SEP-87 Development on TARGON/35 with AT&T Unix System V.3
 * 19-MAY-89 Additions for CP/M 3.0 und MP/M
 * 23-DEC-90 Ported to COHERENT 3.0
 * 10-JUN-92 Some optimization done
 * 25-JUN-92 Flush output of stdout only at every OUT to port 0
 * 25-JUN-92 Comments in english and ported to COHERENT 4.0
 */

/*
 *	This module contains the I/O handlers for a simulation
 *	of the hardware required for a CP/M system.
 *
 *	Used I/O ports:
 *
 *       0 - console status
 *       1 - console data
 *
 *       2 - printer status
 *       3 - printer data
 *
 *       4 - auxilary status
 *       5 - auxilary data
 *
 *      10 - FDC drive
 *      11 - FDC track
 *      12 - FDC sector
 *      13 - FDC command
 *      14 - FDC status
 *
 *      15 - DMA destination address low
 *      16 - DMA destination address high
 *
 *      20 - MMU initialization
 *      21 - MMU bank select
 *
 *      25 - clock command
 *      26 - clock data
 *
 */

#include <stdio.h>
#include <signal.h>
#if defined(COHERENT) && !defined(_I386)
#include <sys/fcntl.h>
#else
#include <fcntl.h>
#endif
#ifndef COHERENT
#include <malloc.h>
#include <memory.h>
#endif
#include <time.h>
#include "sim.h"
#include "simglb.h"

/*
 *	Structure to describe a emulated floppy disk drive:
 *		pointer to filename
 *		pointer to file descriptor
 *		number of tracks
 *		number of sectors
 */
struct dskdef {
	char *fn;
	int *fd;
	unsigned int tracks;
	unsigned int sectors;
};

static BYTE drive;      /* current drive A..P (0..15) */
static BYTE track;      /* current track (0..255) */
static BYTE sector;     /* current sektor (0..255) */
static BYTE status;     /* status of last I/O operation on FDC */
static BYTE dmadl;      /* current DMA adresse destination low */
static BYTE dmadh;      /* current DMA adresse destination high */
static BYTE clkcmd;     /* clock command */
static int drivea;      /* fd for file "drivea.cpm" */
static int driveb;      /* fd for file "driveb.cpm" */
static int drivec;      /* fd for file "drivec.cpm" */
static int drived;      /* fd for file "drived.cpm" */
static int drivee;      /* fd for file "drivee.cpm" */
static int drivef;      /* fd for file "drivef.cpm" */
static int driveg;      /* fd for file "driveg.cpm" */
static int driveh;      /* fd for file "driveh.cpm" */
static int drivei;      /* fd for file "drivei.cpm" */
static int drivej;      /* fd for file "drivej.cpm" */
static int drivek;      /* fd for file "drivek.cpm" */
static int drivel;      /* fd for file "drivel.cpm" */
static int drivem;      /* fd for file "drivem.cpm" */
static int driven;      /* fd for file "driven.cpm" */
static int driveo;      /* fd for file "driveo.cpm" */
static int drivep;      /* fd for file "drivep.cpm" */
static int printer;     /* fd for file "printer.cpm" */
static int auxin;       /* fd for pipe "auxin" */
static int auxout;      /* fd for pipe "auxout" */
static int aux_in_eof;  /* status of pipe "auxin" (<>0 means EOF) */
static int pid_rec;     /* PID of the receiving process for auxiliary */
static char last_char;  /* buffer for 1 character (console status) */

static struct dskdef disks[16] = {
	{ "disks/drivea.cpm", &drivea, 77, 26 },
	{ "disks/driveb.cpm", &driveb, 77, 26 },
	{ "disks/drivec.cpm", &drivec, 77, 26 },
	{ "disks/drived.cpm", &drived, 77, 26 },
	{ "disks/drivee.cpm", &drivee, -1, -1 },
	{ "disks/drivef.cpm", &drivef, -1, -1 },
	{ "disks/driveg.cpm", &driveg, -1, -1 },
	{ "disks/driveh.cpm", &driveh, -1, -1 },
	{ "disks/drivei.cpm", &drivei, -1, -1 },
	{ "disks/drivej.cpm", &drivej, -1, -1 },
	{ "disks/drivek.cpm", &drivek, -1, -1 },
	{ "disks/drivel.cpm", &drivel, -1, -1 },
	{ "disks/drivem.cpm", &drivem, -1, -1 },
	{ "disks/driven.cpm", &driven, -1, -1 },
	{ "disks/driveo.cpm", &driveo, -1, -1 },
	{ "disks/drivep.cpm", &drivep, -1, -1 }
};

/*
 *      MMU:
 *      ===
 *
 *      +--------+
 * 16KB | common |
 *      +--------+
 *      +--------+  +--------+  ..........  +--------+
 *      |        |  |        |              |        |
 * 48KB |        |  |        |  ..........  |        |
 *      | bank 0 |  | bank 1 |              | bank n |
 *      +--------+  +--------+  ..........  +--------+
 */
#define MAXSEG 16               /* max. number of memory banks */
#define SEGSIZ 49152            /* size of one bank = 48KBytes */
static char *mmu[MAXSEG];       /* MMU with pointers to the banks */
static int selbnk;              /* current bank */
static int maxbnk;              /* number of initialized banks */

/*
 *      Forward declaration of the I/O handlers for all used ports
 */
BYTE io_trap();
BYTE cond_in(), cond_out(), cons_in(), cons_out();
BYTE prtd_in(), prtd_out(), prts_in(), prts_out();
BYTE auxd_in(), auxd_out(), auxs_in(), auxs_out();
BYTE fdcd_in(), fdcd_out();
BYTE fdct_in(), fdct_out();
BYTE fdcs_in(), fdcs_out();
BYTE fdco_in(), fdco_out();
BYTE fdcx_in(), fdcx_out();
BYTE dmal_in(), dmal_out();
BYTE dmah_in(), dmah_out();
BYTE mmui_in(), mmui_out(), mmus_in(), mmus_out();
BYTE clkc_in(), clkc_out(), clkd_in(), clkd_out();

/*
 *	This array contains two function pointer for every
 *	active port, one for input and one for output.
 */
static BYTE (*port[256][2]) () = {
	{ cons_in, cons_out },          /* port 0 */
	{ cond_in, cond_out },          /* port 1 */
	{ prts_in, prts_out },          /* port 2 */
	{ prtd_in, prtd_out },          /* port 3 */
	{ auxs_in, auxs_out },          /* port 4 */
	{ auxd_in, auxd_out },          /* port 5 */
	{ io_trap, io_trap  },          /* port 6 */
	{ io_trap, io_trap  },          /* port 7 */
	{ io_trap, io_trap  },          /* port 8 */
	{ io_trap, io_trap  },          /* port 9 */
	{ fdcd_in, fdcd_out },          /* port 10 */
	{ fdct_in, fdct_out },          /* port 11 */
	{ fdcs_in, fdcs_out },          /* port 12 */
	{ fdco_in, fdco_out },          /* port 13 */
	{ fdcx_in, fdcx_out },          /* port 14 */
	{ dmal_in, dmal_out },          /* port 15 */
	{ dmah_in, dmah_out },          /* port 16 */
	{ io_trap, io_trap  },          /* port 17 */
	{ io_trap, io_trap  },          /* port 18 */
	{ io_trap, io_trap  },          /* port 19 */
	{ mmui_in, mmui_out },          /* port 20 */
	{ mmus_in, mmus_out },          /* port 21 */
	{ io_trap, io_trap  },          /* port 22 */
	{ io_trap, io_trap  },          /* port 23 */
	{ io_trap, io_trap  },          /* port 24 */
	{ clkc_in, clkc_out },          /* port 25 */
	{ clkd_in, clkd_out }           /* port 26 */
};

/*
 *	This function initializes the I/O handlers:
 *	1. Initialize all unused ports with the I/O trap handler.
 *	2. Initialize the MMU with NULL pointers.
 *	3. Open the files which emulates the disk drives. The file
 *	   for drive A must be opened, or CP/M can't be booted.
 *	   Errors for opening one of the other 15 drives results
 *	   in a NULL pointer for fd in the dskdef structure,
 *	   so that this drive can't be used.
 *	4. Create and open the file "printer.cpm" for emulation
 *	   of a printer.
 *	5. Fork the process for receiving from the serial port.
 *	6. Open the named pipes "auxin" and "auxout" for simulation
 *	   of a serial port.
 */
void init_io()
{
	void exit(), perror();
	register int i;

	for (i = 27; i <= 255; i++) {
		port[i][0] = io_trap;
		port[i][1] = io_trap;
	}
	for (i = 0; i < MAXSEG; i++)
		mmu[i] = NULL;
	if ((*disks[0].fd = open(disks[0].fn, O_RDWR)) == -1) {
		perror("file disks/drivea.cpm");
		exit(1);
	}
	for (i = 1; i <= 15; i++)
		if ((*disks[i].fd = open(disks[i].fn, O_RDWR)) == -1)
			disks[i].fd = NULL;
	if ((printer = creat("printer.cpm", 0644)) == -1) {
		perror("file printer.cpm");
		exit(1);
	}
	pid_rec = fork();
	switch (pid_rec) {
	case -1:
		puts("can't fork");
		exit(1);
	case 0:
		execlp("receive", "receive", "auxiliary.cpm", 0);
		puts("can't exec receive process");
		exit(1);
	}
	if ((auxin = open("auxin", O_RDONLY | O_NDELAY)) == -1) {
		perror("pipe auxin");
		exit(1);
	}
	if ((auxout = open("auxout", O_WRONLY)) == -1) {
		perror("pipe auxout");
		exit(1);
	}
}

/*
 *	This function stops the I/O handlers:
 *
 *	1. The files emulating the disk drives are closed.
 *	2. The file "printer.com" emulating a printer is closed.
 *	3. The named pipes "auxin" and "auxout" are closed.
 *	4. The receiving process for the serial port is stopped.
 */
void exit_io()
{
	register int i;

	for (i = 0; i <= 15; i++)
		if (disks[i].fd != NULL)
			close(*disks[i].fd);
	close(printer);
	close(auxin);
	close(auxout);
	kill(pid_rec, SIGHUP);
}

/*
 *	This function is called for every IN opcode from the
 *	CPU emulation. It calls the right handler for the
 *	port, from which input is wanted.
 */
BYTE io_in(adr)
register BYTE adr;
{
	return((*port[adr][0]) ());
}

/*
 *	This function is called for every OUT opcode from the
 *	CPU emulation. It calls the right handler for the port,
 *	to which output is wanted.
 */
BYTE io_out(adr, data)
register BYTE adr, data;
{
	(*port[adr][1]) (data);
}

/*
 *      I/O trap handler
 */
static BYTE io_trap()
{
	cpu_error = IOTRAP;
	cpu_state = STOPPED;
}

/*
 *      I/O handler for read console status:
 *      0xff : input available
 *      0x00 : no input available
 */
static BYTE cons_in()
{
	register int flags, readed;

	if (last_char)
		return((BYTE) 0xff);
	if (cntl_c)
		return((BYTE) 0xff);
	if (cntl_bs)
		return((BYTE) 0xff);
	else {
		flags = fcntl(0, F_GETFL, 0);
		fcntl(0, F_SETFL, flags | O_NDELAY);
		readed = read(0, &last_char, 1);
		fcntl(0, F_SETFL, flags);
		if (readed == 1)
			return((BYTE) 0xff);
		else
			return((BYTE) 0);
	}
}

/*
 *      I/O handler for write console status:
 *      no reaction
 */
static BYTE cons_out(data)
register BYTE data;
{
	data = data;
}

/*
 *      I/O handler for read console data:
 *      read one character from the terminal without echo
 *      and character transformations
 */
static BYTE cond_in()
{
	char c;

	aborted:
	if (last_char) {
		c = last_char;
		last_char = '\0';
	} else if (cntl_c) {
		cntl_c--;
		c = 0x03;
	} else if (cntl_bs) {
		cntl_bs--;
		c = 0x1c;
	} else if (read(0, &c, 1) != 1) {
	       goto aborted;
	}
	return((BYTE) c);
}

/*
 *      I/O handler for write console data:
 *      the output is written to the terminal
 */
static BYTE cond_out(data)
register BYTE data;
{
	putchar(data & 0x7f);
	if (data == '\f')
		printf("\033H\033J");
	fflush(stdout);
}

/*
 *      I/O handler for read printer status:
 *      the printer is ready all the time
 */
static BYTE prts_in()
{
	return((BYTE) 0xff);
}

/*
 *      I/O handler for write printer status:
 *      no reaction
 */
static BYTE prts_out(data)
register BYTE data;
{
	data = data;
}

/*
 *      I/O handler for read printer data:
 *      always read a 0 from the printer
 */
static BYTE prtd_in()
{
	return((BYTE) 0);
}

/*
 *      I/O handler for write printer data:
 *	the output is written to file "printer.cpm"
 */
static BYTE prtd_out(data)
BYTE data;
{
	if (data != '\r')
		write(printer, (char *) &data, 1);
}

/*
 *      I/O handler for read aux status:
 *	return EOF status of the aux device
 */
static BYTE auxs_in()
{
	return((BYTE) aux_in_eof);
}

/*
 *      I/O handler for write aux status:
 *      change EOF status of the aux device
 */
static BYTE auxs_out(data)
register BYTE data;
{
	aux_in_eof = data;
}

/*
 *      I/O handler for read aux data:
 *      read next byte from pipe "auxin"
 */
static BYTE auxd_in()
{
	char c;

	if (read(auxin, &c, 1) == 1)
		return((BYTE) c);
	else {
		aux_in_eof = 0xff;
		return((BYTE) 0x1a);    /* CP/M EOF */
	}
}

/*
 *      I/O handler for write aux data:
 *      write output to pipe "auxout"
 */
static BYTE auxd_out(data)
BYTE data;
{
	if (data != '\r')
		write(auxout, (char *) &data, 1);
}

/*
 *      I/O handler for read FDC drive:
 *      return the current drive
 */
static BYTE fdcd_in()
{
	return((BYTE) drive);
}

/*
 *      I/O handler for write FDC drive:
 *      set the current drive
 */
static BYTE fdcd_out(data)
register BYTE data;
{
	drive = data;
}

/*
 *      I/O handler for read FDC track:
 *      return the current track
 */
static BYTE fdct_in()
{
	return((BYTE) track);
}

/*
 *      I/O handler for write FDC track:
 *      set the current track
 */
static BYTE fdct_out(data)
register BYTE data;
{
	track = data;
}

/*
 *      I/O handler for read FDC sector
 *      return the current sector
 */
static BYTE fdcs_in()
{
	return((BYTE) sector);
}

/*
 *      I/O handler for write FDC sector:
 *      set the current sector
 */
static BYTE fdcs_out(data)
register BYTE data;
{
	sector = data;
}

/*
 *      I/O handler for read FDC command:
 *      always returns 0
 */
static BYTE fdco_in()
{
	return((BYTE) 0);
}

/*
 *      I/O handler for write FDC command:
 *      transfer one sector in the wanted direction,
 *      0 = read, 1 = write
 *
 *      The status byte of the FDC is set as follows:
 *        0 - ok
 *        1 - illegal drive
 *        2 - illegal track
 *        3 - illegal sector
 *        4 - seek error
 *        5 - read error
 *        6 - write error
 *        7 - illegal command to FDC
 */
static BYTE fdco_out(data)
register BYTE data;
{
	register long pos;
	long lseek();

	if (disks[drive].fd == NULL) {
		status = 1;
		return;
	}
	if (track > disks[drive].tracks) {
		status = 2;
		return;
	}
	if (sector > disks[drive].sectors) {
		status = 3;
		return;
	}
	pos = (((long)track) * ((long)disks[drive].sectors) + sector - 1) << 7;
	if (lseek(*disks[drive].fd, pos, 0) == -1L) {
		status = 4;
		return;
	}
	switch (data) {
	case 0:                 /* read */
		if (read(*disks[drive].fd, (char *) ram + (dmadh << 8) + dmadl, 128) != 128)
			status = 5;
		else
			status = 0;
		break;
	case 1:                 /* write */
		if (write(*disks[drive].fd, (char *) ram + (dmadh << 8) + dmadl, 128) != 128)
			status = 6;
		else
			status = 0;
		break;
	default:                /* illegal command */
		status = 7;
		break;
	}
}

/*
 *      I/O handler for read FDC status:
 *      returns status of last FDC operation,
 *      0 = ok, else some error
 */
static BYTE fdcx_in()
{
	return((BYTE) status);
}

/*
 *      I/O handler for write FDC status:
 *      no reaction
 */
static BYTE fdcx_out(data)
register BYTE data;
{
	data = data;
}

/*
 *      I/O handler for read lower byte of DMA address:
 *      return lower byte of current DMA address
 */
static BYTE dmal_in()
{
	return((BYTE) dmadl);
}

/*
 *      I/O handler for write lower byte of DMA address:
 *      set lower byte of DMA address
 */
static BYTE dmal_out(data)
register BYTE data;
{
	dmadl = data;
}

/*
 *      I/O handler for read higher byte of DMA address:
 *      return higher byte of current DMA address
 */
static BYTE dmah_in()
{
	return((BYTE) dmadh);
}

/*
 *      I/O handler for write higher byte of DMA address:
 *      set higher byte of the DMA address
 */
static BYTE dmah_out(data)
register BYTE data;
{
	dmadh = data;
}

/*
 *      I/O handler for read MMU initialization:
 *      return number of initialized MMU banks
 */
static BYTE mmui_in()
{
	return((BYTE) maxbnk);
}

/*
 *      I/O handler for write MMU initialization:
 *	for the FIRST call the memory for the wanted number of banks
 *	is allocated and pointers to the memory is stored in the MMU array
 */
static BYTE mmui_out(data)
register BYTE data;
{
	register int i;

	if (mmu[0] != NULL)
		return;
	if (data > MAXSEG) {
		printf("Try to init %d banks, available %d banks\n", data, MAXSEG);
		exit(1);
	}
	for (i = 0; i < data; i++) {
		if ((mmu[i] = malloc(SEGSIZ)) == NULL) {
			printf("can't allocate memory for bank %d\n", i+1);
			exit(1);
		}
	}
	maxbnk = data;
}

/*
 *      I/O handler for read MMU bank select:
 *      return current selected MMU bank
 */
static BYTE mmus_in()
{
	return((BYTE) selbnk);
}

/*
 *      I/O handler for write MMU bank select:
 *	if the current selected bank is not equal the wanted bank,
 *	the current bank is saved. Then the memory of the wanted
 *	bank is copied into the CPU address space and this bank is
 *	set to be the current one now.
 */
static BYTE mmus_out(data)
register BYTE data;
{
	if (data > maxbnk) {
		printf("Try to select unallocated bank %d\n", data);
		exit(1);
	}
	if (data == selbnk)
		return;
	memcpy(mmu[selbnk], (char *) ram, SEGSIZ);
	memcpy((char *) ram, mmu[data], SEGSIZ);
	selbnk = data;
}

/*
 *      I/O handler for read clock command:
 *      return last clock command
 */
static BYTE clkc_in()
{
	return(clkcmd);
}

/*
 *      I/O handler for write clock command:
 *      set the wanted clock command
 */
static BYTE clkc_out(data)
register BYTE data;
{
	clkcmd = data;
}

/*
 *      I/O handler for read clock data:
 *	dependent from the last clock command the following
 *	informations are given from the system clock:
 *              0 - secounds in BCD
 *              1 - minutes in BCD
 *              2 - hours in BCD
 *              3 - low byte number of days since 1.1.1978
 *              4 - high byte number of days since 1.1.1978
 *      for every other clock command a 0 is returned
 */
static BYTE clkd_in()
{
	register struct tm *t;
	register int val;
	extern long time();
	long Time;

	time(&Time);
	t = localtime(&Time);
	switch(clkcmd) {
	case 0:                 /* secounds in BCD */
		val = to_bcd(t->tm_sec);
		break;
	case 1:                 /* minutes in BCD */
		val = to_bcd(t->tm_min);
		break;
	case 2:                 /* hours in BCD */
		val = to_bcd(t->tm_hour);
		break;
	case 3:                 /* low byte days */
		val = get_date(t) & 255;
		break;
	case 4:                 /* high byte days */
		val = get_date(t) >> 8;
		break;
	default:
		val = 0;
		break;
	}
	return((BYTE) val);
}

/*
 *      I/O handler for write clock data:
 *      under UNIX the system clock only can be set by the
 *	super user, so we do nothing here
 */
static BYTE clkd_out(data)
register BYTE data;
{
	data = data;
}

/*
 *      Convert a integer to BCD
 */
static int to_bcd(val)
register int val;
{
	register int i = 0;

	while (val >= 10) {
		i += val / 10;
		i <<= 4;
		val %= 10;
	}
	i += val;
	return (i);
}

/*
 *	Calculate number of days since 1.1.1978
 */
static int get_date(t)
register struct tm *t;
{
	register int i;
	register int val = 0;

	for (i = 1978; i < 1900 + t->tm_year; i++) {
		val += 365;
		if (i % 4 == 0)
			val++;
	}
	val += t->tm_yday + 1;
	return(val);
}
