#include "ibm.h"

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

typedef struct winbond_t {
	int cur_reg;
	int cur_device;
	int index;

	uint8_t unlock_key;
	uint8_t key[2];
	int enable_regs;

	struct {
		int enable;
		uint16_t addr;
		int irq;
		int dma;
	} dev[11];
} winbond_t;

static winbond_t winbond_global;

#define DEV_FDC         0x0
#define DEV_LPT         0x1
#define DEV_COM1        0x2
#define DEV_COM2        0x3
#define DEV_KBC         0x5
#define DEV_GPIO        0x7
#define DEV_GPIO_II     0x8
#define DEV_GPIO_III    0x9
#define DEV_ACPI        0xa

#define REG_DEVICE      0x07
#define REG_DEVICE_ID   0x20
#define REG_DEVICE_REV  0x21
#define REG_ENABLE      0x30
#define REG_ADDRHI      0x60
#define REG_ADDRLO      0x61
#define REG_IRQ         0x70
#define REG_DMA         0x74

void winbond_write_reg(winbond_t *winbond, int index, uint8_t val) {
	if (index == REG_DEVICE)
		winbond->cur_device = val & 0xf;
	else {
//                pclog("Write winbond %02x [%02x] %02x\n", winbond->cur_reg, winbond->cur_device, val);
		switch (index) {
		case REG_ENABLE:
			if (winbond->cur_device <= DEV_ACPI)
				winbond->dev[winbond->cur_device].enable = val;
			break;
		case REG_ADDRLO:
			if (winbond->cur_device <= DEV_ACPI)
				winbond->dev[winbond->cur_device].addr = (winbond->dev[winbond->cur_device].addr & 0xff00) | val;
			break;
		case REG_ADDRHI:
			if (winbond->cur_device <= DEV_ACPI)
				winbond->dev[winbond->cur_device].addr = (winbond->dev[winbond->cur_device].addr & 0x00ff) | (val << 8);
			break;
		case REG_IRQ:
			if (winbond->cur_device <= DEV_ACPI)
				winbond->dev[winbond->cur_device].irq = val;
			break;
		case REG_DMA:
			if (winbond->cur_device <= DEV_ACPI)
				winbond->dev[winbond->cur_device].dma = val;
			break;
		}

		switch (winbond->cur_device) {
		case DEV_FDC:fdc_remove();
			if (winbond->dev[DEV_FDC].enable & 1)
				fdc_add();
			break;
		case DEV_COM1:serial1_remove();
			if (winbond->dev[DEV_COM1].enable & 1)
				serial1_set(winbond->dev[DEV_COM1].addr, winbond->dev[DEV_COM1].irq);
			break;
		case DEV_COM2:serial2_remove();
			if (winbond->dev[DEV_COM2].enable & 1)
				serial2_set(winbond->dev[DEV_COM2].addr, winbond->dev[DEV_COM2].irq);
			break;
		case DEV_LPT:lpt1_remove();
			lpt2_remove();
			if (winbond->dev[DEV_LPT].enable & 1)
				lpt1_init(winbond->dev[DEV_LPT].addr);
			break;
		}
	}
}

uint8_t winbond_read_reg(winbond_t *winbond, int index) {
//        pclog("Read winbond %02x\n", winbond->cur_reg);

	switch (index) {
	case REG_DEVICE:return winbond->cur_device;
	case REG_DEVICE_ID:return 0x97;
	case REG_DEVICE_REV:return 0x73;
	case REG_ENABLE:return winbond->dev[winbond->cur_device].enable;
	case REG_ADDRLO:return winbond->dev[winbond->cur_device].addr & 0xff;
	case REG_ADDRHI:return winbond->dev[winbond->cur_device].addr >> 8;
	case REG_IRQ:return winbond->dev[winbond->cur_device].irq;
	case REG_DMA:return winbond->dev[winbond->cur_device].dma;
	}

	return 0xff;
}

static void winbond_write_3fx(uint16_t port, uint8_t val, void *p) {
	winbond_t *winbond = (winbond_t *)p;

//        pclog("winbond_write: port=%04x val=%02x\n", port, val);

	switch (port) {
	case 0x3f0: /*Enable/index register*/
		if (val == 0xaa) {
			fdc_3f1_enable(1);
			winbond->enable_regs = 0;
		} else if (!winbond->enable_regs) {
			winbond->key[1] = winbond->key[0];
			winbond->key[0] = val;
			winbond->enable_regs = (winbond->key[0] == winbond->unlock_key && winbond->key[1] == winbond->unlock_key);
			/*The FDC conflicts with the 3fx config registers, so disable
			  it when enabling config registers*/
			if (winbond->enable_regs)
				fdc_3f1_enable(0);
		} else
			winbond->index = val;
		break;
	case 0x3f1: /*Data*/
		if (winbond->enable_regs)
			winbond_write_reg(winbond, winbond->index, val);
		break;
	}
}

static uint8_t winbond_read_3fx(uint16_t port, void *p) {
	winbond_t *winbond = (winbond_t *)p;
	uint8_t ret = 0xff;

	switch (port) {
	case 0x3f0: /*Index*/
		if (winbond->enable_regs)
			ret = winbond->index;
		break;

	case 0x3f1: /*Data*/
		if (winbond->enable_regs)
			ret = winbond_read_reg(winbond, winbond->index);
		break;
	}

//        pclog("winbond_read: port=%04x val=%02x\n", port, ret);

	return ret;
}

void w83977tf_init(uint16_t base, uint8_t key) {
	memset(&winbond_global, 0, sizeof(winbond_t));

	winbond_global.unlock_key = key;
	io_sethandler(base, 0x0002, winbond_read_3fx, NULL, NULL, winbond_write_3fx, NULL, NULL, &winbond_global);
}
