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