#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);
}
