#include "ibm.h"
#include "fdc.h"
#include "io.h"
#include "lpt.h"
#include "serial.h"

#include "pc87306.h"

typedef struct pc87306_t {
	int first_read;
	int cur_addr;

	uint8_t regs[32];

	uint8_t gpio_1, gpio_2;
} pc87306_t;

static pc87306_t pc87306_global;

#define REG_FER  0x00
#define REG_FAR  0x01
#define REG_GPBA 0x0f
#define REG_SCF0 0x12
#define REG_PNP0 0x1b

#define FER_LPT_ENA  (1 << 0)
#define FER_COM1_ENA (1 << 1)
#define FER_COM2_ENA (1 << 2)
#define FER_FDC_ENA  (1 << 3)

static uint16_t get_com_addr(pc87306_t *pc87306, int reg) {
	switch (reg) {
	case 0:return 0x3f8;
	case 1:return 0x2f8;
	case 2:
		switch (pc87306->regs[REG_FAR] >> 6) {
		case 0:return 0x3e8;
		case 1:return 0x338;
		case 2:return 0x2e8;
		case 3:return 0x220;
		}
		break;
	case 3:
		switch (pc87306->regs[REG_FAR] >> 6) {
		case 0:return 0x2e8;
		case 1:return 0x238;
		case 2:return 0x2e0;
		case 3:return 0x228;
		}
		break;
	}
	fatal("get_com_addr invalid\n");
	return 0xff;
}

static uint8_t pc87306_gpio_read(uint16_t port, void *p) {
	pc87306_t *pc87306 = (pc87306_t *)p;
	uint8_t temp;

	if (!(port & 1))
		temp = pc87306->gpio_1;
	else
		temp = pc87306->gpio_2;

//        pclog("Read GPIO %04x %02x\n", port, temp);
	return temp;
}

static void pc87306_gpio_write(uint16_t port, uint8_t val, void *p) {
	pc87306_t *pc87306 = (pc87306_t *)p;

//        pclog("Write GPIO %04x %02x\n", port, val);
	if (!(port & 1))
		pc87306->gpio_1 = val;
}

static uint8_t pc87306_read(uint16_t port, void *p) {
	pc87306_t *pc87306 = (pc87306_t *)p;

	if (!(port & 1)) {
		if (pc87306->first_read) {
			pc87306->first_read = 0;
			return 0x88; /*ID*/
		}
		return pc87306->cur_addr;
	} else {
		pc87306->regs[8] = 0x70;
//                pclog("PC87306 read %02x %02x %04x:%04x\n", pc87306->cur_addr, pc87306->regs[pc87306->cur_addr], CS,cpu_state.pc);
//                if (pc87306->cur_addr == 0)
//                        output = 3;
		return pc87306->regs[pc87306->cur_addr];
	}
}

static void pc87306_write(uint16_t port, uint8_t val, void *p) {
	pc87306_t *pc87306 = (pc87306_t *)p;
//pclog("pc87306_write: port=%04x val=%02x %04x:%04x\n", port, val, CS,cpu_state.pc);
	if (!(port & 1))
		pc87306->cur_addr = val & 0x1f;
	else {
		uint16_t old_gpio_addr = pc87306_global.regs[REG_GPBA] << 2;

		pc87306->regs[pc87306->cur_addr] = val;

//                pclog("PC87306 write %02x %02x\n", pc87306->cur_addr, val);

		fdc_remove();
		if (pc87306->regs[REG_FER] & FER_FDC_ENA)
			fdc_add();
		serial1_remove();
		if (pc87306->regs[REG_FER] & FER_COM1_ENA) {
			uint16_t addr = get_com_addr(pc87306, (pc87306->regs[REG_FAR] >> 2) & 3);

			switch ((pc87306->regs[REG_FAR] >> 2) & 3) {
			case 0:
			case 2:serial1_set(addr, 4);
				break;
			case 1:
			case 3:serial1_set(addr, 3);
				break;
			}
		}
		serial2_remove();
		if (pc87306->regs[REG_FER] & FER_COM2_ENA) {
			uint16_t addr = get_com_addr(pc87306, (pc87306->regs[REG_FAR] >> 4) & 3);

			switch ((pc87306->regs[REG_FAR] >> 4) & 3) {
			case 0:
			case 2:serial2_set(addr, 4);
				break;
			case 1:
			case 3:serial2_set(addr, 3);
				break;
			}
		}
		lpt1_remove();
		lpt2_remove();
		if (pc87306->regs[REG_FER] & FER_LPT_ENA) {
			int reg;

			reg = pc87306->regs[REG_FAR] & 3;
			reg |= (pc87306->regs[REG_PNP0] & 0x30) >> 2;

			switch (reg) {
			case 0x0:
			case 0x8:
			case 0x4:
			case 0x5:
			case 0x6:
			case 0x7:lpt1_init(0x378);
				break;
			case 0x1:
			case 0x9:lpt1_init(0x3bc);
				break;
			case 0x2:
			case 0xa:
			case 0xc:
			case 0xd:
			case 0xe:
			case 0xf:lpt1_init(0x278);
				break;
			}
		}
		io_removehandler(old_gpio_addr, 0x0002, pc87306_gpio_read, NULL, NULL, pc87306_gpio_write, NULL, NULL, &pc87306_global);
		io_sethandler(pc87306_global.regs[REG_GPBA] << 2, 0x0002, pc87306_gpio_read, NULL, NULL, pc87306_gpio_write, NULL, NULL, &pc87306_global);
	}
}

void pc87306_init(uint16_t port) {
	memset(&pc87306_global, 0, sizeof(pc87306_t));

	pc87306_global.first_read = 1;
	io_sethandler(port, 0x0002, pc87306_read, NULL, NULL, pc87306_write, NULL, NULL, &pc87306_global);
	pc87306_global.regs[REG_FER] = 0;
	pc87306_global.regs[REG_FAR] = 0x10;
	pc87306_global.regs[REG_GPBA] = 0x78 >> 2;
	pc87306_global.regs[REG_SCF0] = 0x30;
	pc87306_global.gpio_2 = 0xff; // this is a mask only, output will be defined by zappa_brdconfig and endeavor_brdconfig in intel.c
	io_sethandler(0x0078, 0x0002, pc87306_gpio_read, NULL, NULL, pc87306_gpio_write, NULL, NULL, &pc87306_global);
}
