/*
 * 86Box    A hypervisor and IBM PC system emulator that specializes in
 *          running old operating systems and software designed for IBM
 *          PC systems and compatibles from 1981 through fairly recent
 *          system designs based on the PCI bus.
 *
 *          Emulation of Intel System I/O PCI chip.
 *
 * Authors: Miran Grca, <mgrca8@gmail.com>
 *
 *          Copyright 2016-2018 Miran Grca.
 */
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
#define HAVE_STDARG_H
#include <86box/86box.h>
#include "cpu.h"
#include <86box/device.h>
#include <86box/io.h>
#include <86box/apm.h>
#include <86box/dma.h>
#include <86box/mem.h>
#include <86box/pci.h>
#include <86box/pic.h>
#include <86box/timer.h>
#include <86box/pit.h>
#include <86box/port_92.h>
#include <86box/machine.h>
#include <86box/chipset.h>
#include <86box/plat_unused.h>

typedef struct sio_t {
    uint8_t id;
    uint8_t pci_slot;
    uint8_t pad;
    uint8_t pad0;

    uint8_t regs[256];

    uint16_t timer_base;
    uint16_t timer_latch;

    double fast_off_period;

    pc_timer_t timer;
    pc_timer_t fast_off_timer;

    apm_t     *apm;
    port_92_t *port_92;
} sio_t;

#ifdef ENABLE_SIO_LOG
int sio_do_log = ENABLE_SIO_LOG;

static void
sio_log(const char *fmt, ...)
{
    va_list ap;

    if (sio_do_log) {
        va_start(ap, fmt);
        pclog_ex(fmt, ap);
        va_end(ap);
    }
}
#else
#    define sio_log(fmt, ...)
#endif

static void
sio_timer_write(uint16_t addr, uint8_t val, void *priv)
{
    sio_t *dev = (sio_t *) priv;

    if (!(addr & 0x0002)) {
        if (addr & 0x0001)
            dev->timer_latch = (dev->timer_latch & 0xff) | (val << 8);
        else
            dev->timer_latch = (dev->timer_latch & 0xff00) | val;

        timer_set_delay_u64(&dev->timer, ((uint64_t) dev->timer_latch) * TIMER_USEC);
    }
}

static void
sio_timer_writew(uint16_t addr, uint16_t val, void *priv)
{
    sio_t *dev = (sio_t *) priv;

    if (!(addr & 0x0002)) {
        dev->timer_latch = val;

        timer_set_delay_u64(&dev->timer, ((uint64_t) dev->timer_latch) * TIMER_USEC);
    }
}

static uint8_t
sio_timer_read(uint16_t addr, void *priv)
{
    sio_t   *dev = (sio_t *) priv;
    uint16_t sio_timer_latch;
    uint8_t  ret = 0xff;

    if (!(addr & 0x0002)) {
        cycles -= ((int) (PITCONST >> 32));

        sio_timer_latch = timer_get_remaining_us(&dev->timer);

        if (addr & 0x0001)
            ret = sio_timer_latch >> 8;
        else
            ret = sio_timer_latch & 0xff;
    }

    return ret;
}

static uint16_t
sio_timer_readw(uint16_t addr, void *priv)
{
    sio_t   *dev = (sio_t *) priv;
    uint16_t ret = 0xffff;

    if (!(addr & 0x0002)) {
        cycles -= ((int) (PITCONST >> 32));

        ret = timer_get_remaining_us(&dev->timer);
    }

    return ret;
}

static void
sio_write(int func, int addr, uint8_t val, void *priv)
{
    sio_t  *dev = (sio_t *) priv;
    uint8_t old;

    if (func > 0)
        return;

    if (((addr >= 0x0f) && (addr < 0x4c)) && (addr != 0x40))
        return;

    /* The IB (original) variant of the SIO has no PCI IRQ steering. */
    if ((addr >= 0x60) && (addr <= 0x63) && (dev->id < 0x03))
        return;

    old = dev->regs[addr];

    switch (addr) {
        case 0x04: /*Command register*/
            if (dev->id == 0x03)
                dev->regs[addr] = (dev->regs[addr] & 0xf7) | (val & 0x08);
            break;

        case 0x07:
            dev->regs[addr] &= ~(val & 0x38);
            break;

        case 0x40:
            if (dev->id == 0x03) {
                dev->regs[addr] = (val & 0x7f);

                if (!((val ^ old) & 0x40))
                    return;

                dma_alias_remove();
                if (!(val & 0x40))
                    dma_alias_set();
            } else
                dev->regs[addr] = (val & 0x3f);
            break;
        case 0x41:
        case 0x44:
            dev->regs[addr] = (val & 0x1f);
            break;
        case 0x42:
            if (dev->id == 0x03)
                dev->regs[addr] = val;
            else
                dev->regs[addr] = (val & 0x77);
            break;
        case 0x43:
            if (dev->id == 0x03)
                dev->regs[addr] = (val & 0x01);
            break;
        case 0x45:
        case 0x46:
        case 0x47:
        case 0x48:
        case 0x49:
        case 0x4a:
        case 0x4b:
        case 0x4e:
        case 0x54:
        case 0x55:
        case 0x56:
            dev->regs[addr] = val;
            break;
        case 0x4c:
            dev->regs[addr] = (val & 0x7f);
            break;
        case 0x4d:
            dev->regs[addr] = (val & 0x7f);
            break;
        case 0x4f:
            dev->regs[addr] = val;

            if (!((val ^ old) & 0x40))
                return;

            port_92_remove(dev->port_92);
            if (val & 0x40)
                port_92_add(dev->port_92);
            break;
        case 0x57:
            dev->regs[addr] = val;

            dma_remove_sg();
            dma_set_sg_base(val);
            break;
        case 0x60:
        case 0x61:
        case 0x62:
        case 0x63:
            if (dev->id == 0x03) {
                sio_log("Set IRQ routing: INT %c -> %02X\n", 0x41 + (addr & 0x03), val);
                dev->regs[addr] = val & 0x8f;
                if (val & 0x80)
                    pci_set_irq_routing(PCI_INTA + (addr & 0x03), PCI_IRQ_DISABLED);
                else
                    pci_set_irq_routing(PCI_INTA + (addr & 0x03), val & 0xf);
            }
            break;
        case 0x80:
        case 0x81:
            if (addr == 0x80)
                dev->regs[addr] = val & 0xfd;
            else
                dev->regs[addr] = val;

            if (dev->timer_base & 0x01) {
                io_removehandler(dev->timer_base & 0xfffc, 0x0004,
                                 sio_timer_read, sio_timer_readw, NULL,
                                 sio_timer_write, sio_timer_writew, NULL, dev);
            }
            dev->timer_base = (dev->regs[0x81] << 8) | (dev->regs[0x80] & 0xfd);
            if (dev->timer_base & 0x01) {
                io_sethandler(dev->timer_base & 0xfffc, 0x0004,
                              sio_timer_read, sio_timer_readw, NULL,
                              sio_timer_write, sio_timer_writew, NULL, dev);
            }
            break;
        case 0xa0:
            if (dev->id == 0x03) {
                dev->regs[addr] = val & 0x1f;
                apm_set_do_smi(dev->apm, !!(val & 0x01) && !!(dev->regs[0xa2] & 0x80));
                switch ((val & 0x18) >> 3) {
                    case 0x00:
                        dev->fast_off_period = PCICLK * 32768.0 * 60000.0;
                        break;
                    case 0x01:
                    default:
                        dev->fast_off_period = 0.0;
                        break;
                    case 0x02:
                        dev->fast_off_period = PCICLK;
                        break;
                    case 0x03:
                        dev->fast_off_period = PCICLK * 32768.0;
                        break;
                }
                cpu_fast_off_count = cpu_fast_off_val + 1;
                cpu_fast_off_period_set(cpu_fast_off_val, dev->fast_off_period);
            }
            break;
        case 0xa2:
            if (dev->id == 0x03) {
                dev->regs[addr] = val & 0xff;
                apm_set_do_smi(dev->apm, !!(dev->regs[0xa0] & 0x01) && !!(val & 0x80));
            }
            break;
        case 0xaa:
            if (dev->id == 0x03)
                dev->regs[addr] &= (val & 0xff);
            break;
        case 0xac:
        case 0xae:
            if (dev->id == 0x03)
                dev->regs[addr] = val & 0xff;
            break;
        case 0xa4:
            if (dev->id == 0x03) {
                dev->regs[addr]    = val & 0xfb;
                cpu_fast_off_flags = (cpu_fast_off_flags & 0xffffff00) | dev->regs[addr];
            }
            break;
        case 0xa5:
            if (dev->id == 0x03) {
                dev->regs[addr]    = val & 0xff;
                cpu_fast_off_flags = (cpu_fast_off_flags & 0xffff00ff) | (dev->regs[addr] << 8);
            }
            break;
        case 0xa7:
            if (dev->id == 0x03) {
                dev->regs[addr]    = val & 0xa0;
                cpu_fast_off_flags = (cpu_fast_off_flags & 0x00ffffff) | (dev->regs[addr] << 24);
            }
            break;
        case 0xa8:
            dev->regs[addr]    = val & 0xff;
            cpu_fast_off_val   = val;
            cpu_fast_off_count = val + 1;
            cpu_fast_off_period_set(cpu_fast_off_val, dev->fast_off_period);
            break;
        default:
            break;
    }
}

static uint8_t
sio_read(int func, int addr, void *priv)
{
    const sio_t  *dev = (sio_t *) priv;
    uint8_t       ret;

    ret = 0xff;

    if (func == 0)
        ret = dev->regs[addr];

    return ret;
}

static void
sio_config_write(UNUSED(uint16_t addr), UNUSED(uint8_t val), UNUSED(void *priv))
{
    //
}

static uint8_t
sio_config_read(uint16_t port, UNUSED(void *priv))
{
    uint8_t ret = 0x00;

    switch (port & 0x000f) {
        case 3:
            ret = 0xff;
            break;
        case 5:
            /*
               Dell Dimension XPS P60 jumpers:
                   - Bit 5: Disable CMOS Setup (1 = yes, 0 = no).

               Dell OptiPlex 560/L jumpers:
                   - Bit 1: Password (1 = disable, 0 = enable);
                   - Bit 5: Clear CMOS (1 = no, 0 = yes).
                   - Bits 7, 6: Board type:
                       - 0, 0 = L;
                       - 0, 1 = MT;
                       - 1, 0 = M;
                       - 1, 1 = M.
             */
            if (machines[machine].init == machine_at_opti560l_init)
                ret = 0x20;
            else
                ret = 0xd3;

            switch (cpu_pci_speed) {
                case 20000000:
                    ret |= 0x0c;
                    break;
                case 25000000:
                default:
                    ret |= 0x00;
                    break;
                case 30000000:
                    ret |= 0x08;
                    break;
                case 33333333:
                    ret |= 0x04;
                    break;
            }
            break;

        default:
            break;
    }

    return ret;
}

static void
sio_reset_hard(void *priv)
{
    sio_t *dev = (sio_t *) priv;

    memset(dev->regs, 0, 256);

    dev->regs[0x00] = 0x86;
    dev->regs[0x01] = 0x80; /*Intel*/
    dev->regs[0x02] = 0x84;
    dev->regs[0x03] = 0x04; /*82378IB (SIO)*/
    dev->regs[0x04] = 0x07;
    dev->regs[0x07] = 0x02;
    dev->regs[0x08] = dev->id;

    dev->regs[0x40] = 0x20;
    dev->regs[0x41] = 0x00;
    dev->regs[0x42] = 0x04;
    dev->regs[0x45] = 0x10;
    dev->regs[0x46] = 0x0f;
    dev->regs[0x48] = 0x01;
    dev->regs[0x4a] = 0x10;
    dev->regs[0x4b] = 0x0f;
    dev->regs[0x4c] = 0x56;
    dev->regs[0x4d] = 0x40;
    dev->regs[0x4e] = 0x07;
    dev->regs[0x4f] = 0x4f;
    dev->regs[0x57] = 0x04;
    if (dev->id == 0x03) {
        dev->regs[0x60] = 0x80;
        dev->regs[0x61] = 0x80;
        dev->regs[0x62] = 0x80;
        dev->regs[0x63] = 0x80;
    }
    dev->regs[0x80] = 0x78;
    if (dev->id == 0x03) {
        dev->regs[0xa0] = 0x08;
        dev->regs[0xa8] = 0x0f;
    }

    pci_set_irq_routing(PCI_INTA, PCI_IRQ_DISABLED);
    pci_set_irq_routing(PCI_INTB, PCI_IRQ_DISABLED);
    pci_set_irq_routing(PCI_INTC, PCI_IRQ_DISABLED);
    pci_set_irq_routing(PCI_INTD, PCI_IRQ_DISABLED);

    if (dev->timer_base & 0x0001) {
        io_removehandler(dev->timer_base & 0xfffc, 0x0004,
                         sio_timer_read, sio_timer_readw, NULL,
                         sio_timer_write, sio_timer_writew, NULL, dev);
    }

    dev->timer_base = 0x0078;
}

static void
sio_apm_out(UNUSED(uint16_t port), UNUSED(uint8_t val), void *priv)
{
    sio_t *dev = (sio_t *) priv;

    if (dev->apm->do_smi)
        dev->regs[0xaa] |= 0x80;
}

static void
sio_fast_off_count(void *priv)
{
    sio_t *dev = (sio_t *) priv;

    smi_raise();
    dev->regs[0xaa] |= 0x20;
}

static void
sio_reset(void *priv)
{
    const sio_t *dev = (sio_t *) priv;

    /* Disable the PIC mouse latch. */
    sio_write(0, 0x4d, 0x40, priv);

    sio_write(0, 0x57, 0x04, priv);

    dma_set_params(1, 0xffffffff);

    if (dev->id == 0x03) {
        sio_write(0, 0xa0, 0x08, priv);
        sio_write(0, 0xa2, 0x00, priv);
        sio_write(0, 0xa4, 0x00, priv);
        sio_write(0, 0xa5, 0x00, priv);
        sio_write(0, 0xa6, 0x00, priv);
        sio_write(0, 0xa7, 0x00, priv);
        sio_write(0, 0xa8, 0x0f, priv);
    }
}

static void
sio_close(void *priv)
{
    sio_t *dev = (sio_t *) priv;

    free(dev);
}

static void
sio_speed_changed(void *priv)
{
    sio_t *dev = (sio_t *) priv;
    int    te;

    te = timer_is_enabled(&dev->timer);

    timer_disable(&dev->timer);
    if (te)
        timer_set_delay_u64(&dev->timer, ((uint64_t) dev->timer_latch) * TIMER_USEC);

    if (dev->id == 0x03) {
        te = timer_is_on(&dev->fast_off_timer);

        timer_stop(&dev->fast_off_timer);
        if (te)
            timer_on_auto(&dev->fast_off_timer, dev->fast_off_period);
    }
}

static void *
sio_init(const device_t *info)
{
    sio_t *dev = (sio_t *) calloc(1, sizeof(sio_t));

    pci_add_card(PCI_ADD_SOUTHBRIDGE, sio_read, sio_write, dev, &dev->pci_slot);

    dev->id = info->local;

    if (dev->id == 0x03)
        timer_add(&dev->fast_off_timer, sio_fast_off_count, dev, 0);

    sio_reset_hard(dev);

    cpu_fast_off_flags = 0x00000000;

    if (dev->id == 0x03) {
        cpu_fast_off_val   = dev->regs[0xa8];
        cpu_fast_off_count = cpu_fast_off_val + 1;

        cpu_register_fast_off_handler(&dev->fast_off_timer);
    } else
        cpu_fast_off_val = cpu_fast_off_count = 0;

    if (dev->id == 0x03) {
        dev->apm = device_add(&apm_pci_device);
        /* APM intercept handler to update 82378ZB SMI status on APM SMI. */
        io_sethandler(0x00b2, 0x0001, NULL, NULL, NULL, sio_apm_out, NULL, NULL, dev);
    }

    dev->port_92 = device_add(&port_92_pci_device);

    dma_set_sg_base(0x04);
    dma_set_params(1, 0xffffffff);
    dma_ext_mode_init();
    dma_high_page_init();

    if (dev->id == 0x03)
        dma_alias_set();

    io_sethandler(0x0073, 0x0001,
                  sio_config_read, NULL, NULL, sio_config_write, NULL, NULL, dev);
    io_sethandler(0x0075, 0x0001,
                  sio_config_read, NULL, NULL, sio_config_write, NULL, NULL, dev);

    timer_add(&dev->timer, NULL, NULL, 0);

#if 0
    device_add(&i8254_sec_device);
#endif

    return dev;
}

const device_t sio_device = {
    .name          = "Intel 82378IB (SIO)",
    .internal_name = "sio",
    .flags         = DEVICE_PCI,
    .local         = 0x00,
    .init          = sio_init,
    .close         = sio_close,
    .reset         = sio_reset,
    .available     = NULL,
    .speed_changed = sio_speed_changed,
    .force_redraw  = NULL,
    .config        = NULL
};

const device_t sio_zb_device = {
    .name          = "Intel 82378ZB (SIO)",
    .internal_name = "sio_zb",
    .flags         = DEVICE_PCI,
    .local         = 0x03,
    .init          = sio_init,
    .close         = sio_close,
    .reset         = sio_reset,
    .available     = NULL,
    .speed_changed = sio_speed_changed,
    .force_redraw  = NULL,
    .config        = NULL
};
