#include <stdio.h>
#include <stdint.h>
#include "16550.h"

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

#define IER_RX  (1 << 0) /*Receive data full*/
#define IER_TX  (1 << 1) /*Transmit data empty*/
#define IER_LSR (1 << 2) /*Line status*/
#define IER_MSR (1 << 3) /*Modem status*/

#define LSR_RX_READY (1 << 0) /*Receive register full*/
#define LSR_TX_EMPTY (1 << 5) /*Transmitter holding register empty*/

#define IIR_NO_IRQ 1
#define IIR_LSR    6
#define IIR_RX     4
#define IIR_TX     2
#define IIR_MSR    0

void n16550_init(n16550_t *n16550, int input_clock, void (*set_irq)(void *p, int state), void (*tx_data)(void *p, uint8_t val), void *p, void (*log)(const char *format, ...))
{
	n16550->set_irq = set_irq;
	n16550->tx_data = tx_data;
	n16550->p = p;
	n16550->log = log;
	n16550->input_clock = input_clock;
	n16550->baud_rate = n16550->input_clock / (3 * 16);
}

static void update_ints(n16550_t *n16550)
{
	int stat = 0;

	n16550->iir = IIR_NO_IRQ;

	if ((n16550->ier & IER_LSR) && (n16550->int_status & SERIAL_INT_LSR)) /*Line status interrupt*/
	{
		stat = 1;
		n16550->iir = IIR_LSR;
	}
	else if ((n16550->ier & IER_RX) && (n16550->int_status & SERIAL_INT_RECEIVE)) /*Received data available*/
	{
		stat = 1;
		n16550->iir = IIR_RX;
	}
	else if ((n16550->ier & IER_TX) && (n16550->int_status & SERIAL_INT_TRANSMIT)) /*Transmit data empty*/
	{
		stat = 1;
		n16550->iir = IIR_TX;
	}
	else if ((n16550->ier & IER_MSR) && (n16550->int_status & SERIAL_INT_MSR)) /*Modem status interrupt*/
	{
		stat = 1;
		n16550->iir = IIR_MSR;
	}

//        lark_log("update_ints: %i %02x\n", stat, n16550->mctrl);
	n16550->set_irq(n16550->p, stat);
}

uint8_t n16550_read(n16550_t *n16550, uint32_t addr)
{
	uint8_t temp = 0xff;

	switch (addr & 7)
	{
		case 0:
		if (n16550->lcr & 0x80)
		{
			temp = n16550->dlab1;
			break;
		}

		n16550->lsr &= ~LSR_RX_READY;
		n16550->int_status &= ~SERIAL_INT_RECEIVE;
		update_ints(n16550);
		temp = n16550->rx_data;
//                lark_log("n16550_read: temp=%02x\n", temp);
		break;
		case 1:
		if (n16550->lcr & 0x80)
			temp = n16550->dlab2;
		else
			temp = n16550->ier;
		break;
		case 2:
		temp = n16550->iir;
		if ((temp & 0xe) == IIR_TX)
		{
			n16550->int_status &= ~SERIAL_INT_TRANSMIT;
			update_ints(n16550);
		}
		if (n16550->fcr & 1)
			temp |= 0xc0;
		break;
		case 3:
		temp = n16550->lcr;
		break;
		case 4:
		temp = n16550->mctrl;
		break;
		case 5:
		if (n16550->lsr & 0x20)
			n16550->lsr |= 0x40;
		n16550->lsr |= 0x20;
		temp = n16550->lsr;
		if (n16550->lsr & 0x1f)
			n16550->lsr &= ~0x1e;
		n16550->int_status &= ~SERIAL_INT_LSR;
		update_ints(n16550);
		break;
		case 6:
		temp = n16550->msr;
		n16550->msr &= ~0x0f;
		n16550->int_status &= ~SERIAL_INT_MSR;
		update_ints(n16550);
		break;
		case 7:
		temp = n16550->scratch;
		break;
	}

	return temp;
}

void n16550_write(n16550_t *n16550, uint32_t addr, uint8_t val)
{
//        if (n16550->log)
//                n16550->log("n16550_write: addr=%01x val=%02x\n", addr & 7, val);
	switch (addr & 7)
	{
		case 0:
		if (n16550->lcr & 0x80)
		{
			n16550->dlab1 = val;
			if (n16550->dlab1 | (n16550->dlab2 << 8))
				n16550->baud_rate = n16550->input_clock / ((n16550->dlab1 | (n16550->dlab2 << 8)) * 16);
			else
				n16550->baud_rate = n16550->input_clock / (3 * 16);
//                        if (n16550->log)
//                                n16550->log("baud_rate = %i\n", n16550->baud_rate);
			return;
		}
		n16550->thr = val;
		n16550->lsr &= ~LSR_TX_EMPTY;
		//n16550->int_status |= SERIAL_INT_TRANSMIT;
		//update_ints(n16550);
		n16550->tx_data(n16550->p, val);
		n16550->tx_irq_pending = (1000000 * 10) / n16550->baud_rate;
		break;
		case 1:
		if (n16550->lcr & 0x80)
		{
			n16550->dlab2 = val;
			if (n16550->dlab1 | (n16550->dlab2 << 8))
				n16550->baud_rate = n16550->input_clock / ((n16550->dlab1 | (n16550->dlab2 << 8)) * 16);
			else
				n16550->baud_rate = n16550->input_clock / (3 * 16);
//                        if (n16550->log)
//                                n16550->log("baud_rate = %i\n", n16550->baud_rate);
			return;
		}
		n16550->ier = val & 0xf;
		update_ints(n16550);
		break;
		case 2:
		n16550->fcr = val;
		break;
		case 3:
		n16550->lcr = val;
		break;
		case 4:
		n16550->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 ((n16550->msr ^ new_msr) & 0x10)
				new_msr |= 0x01;
			if ((n16550->msr ^ new_msr) & 0x20)
				new_msr |= 0x02;
			if ((n16550->msr ^ new_msr) & 0x80)
				new_msr |= 0x08;
			if ((n16550->msr & 0x40) && !(new_msr & 0x40))
				new_msr |= 0x04;

			n16550->msr = new_msr;
		}
		break;
		case 5:
		n16550->lsr = val;
		if (n16550->lsr & LSR_RX_READY)
			n16550->int_status |= SERIAL_INT_RECEIVE;
		if (n16550->lsr & 0x1e)
			n16550->int_status |= SERIAL_INT_LSR;
		if (n16550->lsr & LSR_TX_EMPTY)
			n16550->int_status |= SERIAL_INT_TRANSMIT;
		update_ints(n16550);
		break;
		case 6:
		n16550->msr = val;
		if (n16550->msr & 0x0f)
			n16550->int_status |= SERIAL_INT_MSR;
		update_ints(n16550);
		break;
		case 7:
		n16550->scratch = val;
		break;
	}
}

void n16550_run(n16550_t *n16550, int timeslice_us)
{
	if (n16550->tx_irq_pending)
	{
		n16550->tx_irq_pending -= timeslice_us;
		if (n16550->tx_irq_pending <= 0)
		{
//                        lark_log("n16550_write: tx irq\n");
			n16550->tx_irq_pending = 0;
			n16550->lsr |= LSR_TX_EMPTY;
			n16550->int_status |= SERIAL_INT_TRANSMIT;
			update_ints(n16550);
		}
	}
	if (n16550->rx_pending)
	{
		n16550->rx_pending -= timeslice_us;
		if (n16550->rx_pending <= 0)
			n16550->rx_pending = 0;
	}
	if (!n16550->rx_pending && n16550->rx_rp != n16550->rx_wp)
	{
		n16550->rx_data = n16550->rx_queue[n16550->rx_rp & 0xff];
		n16550->rx_rp++;

		n16550->lsr |= LSR_RX_READY;
		n16550->int_status |= SERIAL_INT_RECEIVE;
		update_ints(n16550);

		n16550->rx_pending = (1000000 * 10) / n16550->baud_rate;
	}
}

void n16550_receive(n16550_t *n16550, uint8_t val)
{
	if ((n16550->rx_wp - n16550->rx_rp) >= 256)
	{
//                lark_log("n16550 drop %02x\n", val);
		return;
	}

//        lark_log("n16550 receive %02x %i %i\n", val, n16550->rx_wp, n16550->rx_rp);
	n16550->rx_queue[n16550->rx_wp & 0xff] = val;
	n16550->rx_wp++;
}
