#include "ibm.h"
#include "io.h"
#include "mouse.h"
#include "pic.h"
#include "serial.h"
#include "timer.h"

enum {
	SERIAL_INT_LSR = 1,
	SERIAL_INT_RECEIVE = 2,
	SERIAL_INT_TRANSMIT = 4,
	SERIAL_INT_MSR = 8
};

SERIAL serial1, serial2;

void serial_reset() {
	serial1.iir = serial1.ier = serial1.lcr = 0;
	serial2.iir = serial2.ier = serial2.lcr = 0;
	serial1.fifo_read = serial1.fifo_write = 0;
	serial2.fifo_read = serial2.fifo_write = 0;
}

void serial_update_ints(SERIAL *serial) {
	int stat = 0;

	serial->iir = 1;

	if ((serial->ier & 4) && (serial->int_status & SERIAL_INT_LSR)) /*Line status interrupt*/
	{
		stat = 1;
		serial->iir = 6;
	} else if ((serial->ier & 1) && (serial->int_status & SERIAL_INT_RECEIVE)) /*Received data available*/
	{
		stat = 1;
		serial->iir = 4;
	} else if ((serial->ier & 2) && (serial->int_status & SERIAL_INT_TRANSMIT)) /*Transmit data empty*/
	{
		stat = 1;
		serial->iir = 2;
	} else if ((serial->ier & 8) && (serial->int_status & SERIAL_INT_MSR)) /*Modem status interrupt*/
	{
		stat = 1;
		serial->iir = 0;
	}

	if (stat && ((serial->mctrl & 8) || PCJR))
		picintlevel(1 << serial->irq);
	else
		picintc(1 << serial->irq);
}

void serial_write_fifo(SERIAL *serial, uint8_t dat) {
//        pclog("serial_write_fifo %02X\n", serial->lsr);
	serial->fifo[serial->fifo_write] = dat;
	serial->fifo_write = (serial->fifo_write + 1) & 0xFF;
	if (!(serial->lsr & 1)) {
		serial->lsr |= 1;
		serial->int_status |= SERIAL_INT_RECEIVE;
		serial_update_ints(serial);
	}
}

uint8_t serial_read_fifo(SERIAL *serial) {
	if (serial->fifo_read != serial->fifo_write) {
		serial->dat = serial->fifo[serial->fifo_read];
		serial->fifo_read = (serial->fifo_read + 1) & 0xFF;
	}
	return serial->dat;
}

void serial_write(uint16_t addr, uint8_t val, void *p) {
	SERIAL *serial = (SERIAL *)p;
//        pclog("Write serial %03X %02X %04X:%04X\n",addr,val,CS,pc);
	switch (addr & 7) {
	case 0:
		if (serial->lcr & 0x80) {
			serial->dlab1 = val;
			return;
		}
		serial->thr = val;
		serial->lsr |= 0x20;
		serial->int_status |= SERIAL_INT_TRANSMIT;
		serial_update_ints(serial);
		if (serial->mctrl & 0x10) {
			serial_write_fifo(serial, val);
		}
		break;
	case 1:
		if (serial->lcr & 0x80) {
			serial->dlab2 = val;
			return;
		}
		serial->ier = val & 0xf;
		serial_update_ints(serial);
		break;
	case 2:
		if (serial->has_fifo)
			serial->fcr = val;
		break;
	case 3:serial->lcr = val;
		break;
	case 4:
		if ((val & 2) && !(serial->mctrl & 2)) {
			if (serial->rcr_callback)
				serial->rcr_callback((struct SERIAL *)serial, serial->rcr_callback_p);
//                        pclog("RCR raised! sending M\n");
		}
		serial->mctrl = val;
		if (val & 0x10) {
			uint8_t new_msr;

			new_msr = (val & 0x0c) << 4;
			new_msr |= (val & 0x02) ? 0x10 : 0;
			new_msr |= (val & 0x01) ? 0x20 : 0;

			if ((serial->msr ^ new_msr) & 0x10)
				new_msr |= 0x01;
			if ((serial->msr ^ new_msr) & 0x20)
				new_msr |= 0x02;
			if ((serial->msr ^ new_msr) & 0x80)
				new_msr |= 0x08;
			if ((serial->msr & 0x40) && !(new_msr & 0x40))
				new_msr |= 0x04;

			serial->msr = new_msr;
		}
		break;
	case 5:serial->lsr = val;
		if (serial->lsr & 0x01)
			serial->int_status |= SERIAL_INT_RECEIVE;
		if (serial->lsr & 0x1e)
			serial->int_status |= SERIAL_INT_LSR;
		if (serial->lsr & 0x20)
			serial->int_status |= SERIAL_INT_TRANSMIT;
		serial_update_ints(serial);
		break;
	case 6:serial->msr = val;
		if (serial->msr & 0x0f)
			serial->int_status |= SERIAL_INT_MSR;
		serial_update_ints(serial);
		break;
	case 7:serial->scratch = val;
		break;
	}
}

uint8_t serial_read(uint16_t addr, void *p) {
	SERIAL *serial = (SERIAL *)p;
	uint8_t temp = 0;
//        pclog("Read serial %03X %04X(%08X):%04X %i %i  ", addr, CS, cs, pc, mousedelay, ins);
	switch (addr & 7) {
	case 0:
		if (serial->lcr & 0x80) {
			temp = serial->dlab1;
			break;
		}

		serial->lsr &= ~1;
		serial->int_status &= ~SERIAL_INT_RECEIVE;
		serial_update_ints(serial);
		temp = serial_read_fifo(serial);
		if (serial->fifo_read != serial->fifo_write)
			timer_set_delay_u64(&serial->receive_timer, 1000 * TIMER_USEC);
		break;
	case 1:
		if (serial->lcr & 0x80)
			temp = serial->dlab2;
		else
			temp = serial->ier;
		break;
	case 2:temp = serial->iir;
		if ((temp & 0xe) == 2) {
			serial->int_status &= ~SERIAL_INT_TRANSMIT;
			serial_update_ints(serial);
		}
		if (serial->fcr & 1)
			temp |= 0xc0;
		break;
	case 3:temp = serial->lcr;
		break;
	case 4:temp = serial->mctrl;
		break;
	case 5:
		if (serial->lsr & 0x20)
			serial->lsr |= 0x40;
		serial->lsr |= 0x20;
		temp = serial->lsr;
		if (serial->lsr & 0x1f)
			serial->lsr &= ~0x1e;
//                serial.lsr |= 0x60;
		serial->int_status &= ~SERIAL_INT_LSR;
		serial_update_ints(serial);
		break;
	case 6:temp = serial->msr;
		serial->msr &= ~0x0f;
		serial->int_status &= ~SERIAL_INT_MSR;
		serial_update_ints(serial);
		break;
	case 7:temp = serial->scratch;
		break;
	}
//        pclog("%02X\n",temp);
	return temp;
}

void serial_receive_callback(void *p) {
	SERIAL *serial = (SERIAL *)p;

	if (serial->fifo_read != serial->fifo_write) {
		serial->lsr |= 1;
		serial->int_status |= SERIAL_INT_RECEIVE;
		serial_update_ints(serial);
	}
}

/*Tandy might need COM1 at 2f8*/
void serial1_init(uint16_t addr, int irq, int has_fifo) {
	memset(&serial1, 0, sizeof(serial1));
	io_sethandler(addr, 0x0008, serial_read, NULL, NULL, serial_write, NULL, NULL, &serial1);
	serial1.irq = irq;
	serial1.addr = addr;
	serial1.rcr_callback = NULL;
	timer_add(&serial1.receive_timer, serial_receive_callback, &serial1, 0);
	serial1.has_fifo = has_fifo;
}
void serial1_set(uint16_t addr, int irq) {
	serial1_remove();
	io_sethandler(addr, 0x0008, serial_read, NULL, NULL, serial_write, NULL, NULL, &serial1);
	serial1.irq = irq;
	serial1.addr = addr;
}
void serial1_set_has_fifo(int has_fifo) {
	serial1.has_fifo = has_fifo;
}
void serial1_remove() {
	io_removehandler(serial1.addr, 0x0008, serial_read, NULL, NULL, serial_write, NULL, NULL, &serial1);
}

void serial2_init(uint16_t addr, int irq, int has_fifo) {
	memset(&serial2, 0, sizeof(serial2));
	io_sethandler(addr, 0x0008, serial_read, NULL, NULL, serial_write, NULL, NULL, &serial2);
	serial2.irq = irq;
	serial2.addr = addr;
	serial2.rcr_callback = NULL;
	timer_add(&serial2.receive_timer, serial_receive_callback, &serial2, 0);
	serial2.has_fifo = has_fifo;
}
void serial2_set(uint16_t addr, int irq) {
	serial2_remove();
	io_sethandler(addr, 0x0008, serial_read, NULL, NULL, serial_write, NULL, NULL, &serial2);
	serial2.irq = irq;
	serial2.addr = addr;
}
void serial2_set_has_fifo(int has_fifo) {
	serial2.has_fifo = has_fifo;
}
void serial2_remove() {
	io_removehandler(serial2.addr, 0x0008, serial_read, NULL, NULL, serial_write, NULL, NULL, &serial2);
}
