/*
 * 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.
 *
 *          This file is part of the 86Box distribution.
 *
 *          Implementation of the SiS 5513 IDE controller.
 *
 * Authors: Miran Grca, <mgrca8@gmail.com>
 *
 *          Copyright 2024 Miran Grca.
 */
#include <stdarg.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
#define HAVE_STDARG_H
#include <86box/86box.h>
#include <86box/device.h>
#include <86box/io.h>
#include <86box/timer.h>
#include <86box/dma.h>
#include <86box/mem.h>
#include <86box/nvr.h>
#include <86box/hdd.h>
#include <86box/hdc.h>
#include <86box/hdc_ide.h>
#include <86box/hdc_ide_sff8038i.h>
#include <86box/pci.h>
#include <86box/pic.h>
#include <86box/pit.h>
#include <86box/pit_fast.h>
#include <86box/plat.h>
#include <86box/plat_unused.h>
#include <86box/port_92.h>
#include <86box/smram.h>
#include <86box/spd.h>
#include <86box/apm.h>
#include <86box/ddma.h>
#include <86box/acpi.h>
#include <86box/smbus.h>
#include <86box/sis_55xx.h>
#include <86box/chipset.h>

#ifdef ENABLE_SIS_5513_IDE_LOG
int sis_5513_ide_do_log = ENABLE_SIS_5513_IDE_LOG;

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

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

typedef struct sis_5513_ide_t {
    uint8_t            rev;

    uint8_t            pci_conf[256];

    sis_55xx_common_t *sis;
} sis_5513_ide_t;

static void
sis_5513_ide_irq_handler(sis_5513_ide_t *dev)
{
    if (dev->pci_conf[0x09] & 0x01) {
        /* Primary IDE is native. */
        sis_5513_ide_log("Primary IDE IRQ mode: Native, Native\n");
        sff_set_irq_mode(dev->sis->bm[0], IRQ_MODE_SIS_551X);
    } else {
        /* Primary IDE is legacy. */
        sis_5513_ide_log("Primary IDE IRQ mode: IRQ14, IRQ15\n");
        sff_set_irq_mode(dev->sis->bm[0], IRQ_MODE_LEGACY);
    }

    if (dev->pci_conf[0x09] & 0x04) {
        /* Secondary IDE is native. */
        sis_5513_ide_log("Secondary IDE IRQ mode: Native, Native\n");
        sff_set_irq_mode(dev->sis->bm[1], IRQ_MODE_SIS_551X);
    } else {
        /* Secondary IDE is legacy. */
        sis_5513_ide_log("Secondary IDE IRQ mode: IRQ14, IRQ15\n");
        sff_set_irq_mode(dev->sis->bm[1], IRQ_MODE_LEGACY);
    }
}

static void
sis_5513_ide_handler(sis_5513_ide_t *dev)
{
    uint8_t ide_io_on = dev->pci_conf[0x04] & 0x01;

    uint16_t native_base_pri_addr = (dev->pci_conf[0x11] | dev->pci_conf[0x10] << 8) & 0xfffe;
    uint16_t native_side_pri_addr = (dev->pci_conf[0x15] | dev->pci_conf[0x14] << 8) & 0xfffe;
    uint16_t native_base_sec_addr = (dev->pci_conf[0x19] | dev->pci_conf[0x18] << 8) & 0xfffe;
    uint16_t native_side_sec_addr = (dev->pci_conf[0x1c] | dev->pci_conf[0x1b] << 8) & 0xfffe;

    uint16_t current_pri_base;
    uint16_t current_pri_side;
    uint16_t current_sec_base;
    uint16_t current_sec_side;

    /* Primary Channel Programming */
    current_pri_base = (!(dev->pci_conf[0x09] & 1)) ? 0x01f0 : native_base_pri_addr;
    current_pri_side = (!(dev->pci_conf[0x09] & 1)) ? 0x03f6 : native_side_pri_addr;

    /* Secondary Channel Programming */
    current_sec_base = (!(dev->pci_conf[0x09] & 4)) ? 0x0170 : native_base_sec_addr;
    current_sec_side = (!(dev->pci_conf[0x09] & 4)) ? 0x0376 : native_side_sec_addr;

    sis_5513_ide_log("sis_5513_ide_handler(): Disabling primary IDE...\n");
    ide_pri_disable();
    sis_5513_ide_log("sis_5513_ide_handler(): Disabling secondary IDE...\n");
    ide_sec_disable();

    if (ide_io_on) {
        /* Primary Channel Setup */
        if (dev->pci_conf[0x4a] & 0x02) {
            sis_5513_ide_log("sis_5513_ide_handler(): Primary IDE base now %04X...\n", current_pri_base);
            ide_set_base(0, current_pri_base);
            sis_5513_ide_log("sis_5513_ide_handler(): Primary IDE side now %04X...\n", current_pri_side);
            ide_set_side(0, current_pri_side);

            sis_5513_ide_log("sis_5513_ide_handler(): Enabling primary IDE...\n");
            ide_pri_enable();

            sis_5513_ide_log("SiS 5513 PRI: BASE %04x SIDE %04x\n", current_pri_base, current_pri_side);
        }

        /* Secondary Channel Setup */
        if (dev->pci_conf[0x4a] & 0x04) {
            sis_5513_ide_log("sis_5513_ide_handler(): Secondary IDE base now %04X...\n", current_sec_base);
            ide_set_base(1, current_sec_base);
            sis_5513_ide_log("sis_5513_ide_handler(): Secondary IDE side now %04X...\n", current_sec_side);
            ide_set_side(1, current_sec_side);

            sis_5513_ide_log("sis_5513_ide_handler(): Enabling secondary IDE...\n");
            ide_sec_enable();

            sis_5513_ide_log("SiS 5513: BASE %04x SIDE %04x\n", current_sec_base, current_sec_side);
        }
    }

    sff_bus_master_handler(dev->sis->bm[0], ide_io_on,
                           ((dev->pci_conf[0x20] & 0xf0) | (dev->pci_conf[0x21] << 8)) + 0);
    sff_bus_master_handler(dev->sis->bm[1], ide_io_on,
                           ((dev->pci_conf[0x20] & 0xf0) | (dev->pci_conf[0x21] << 8)) + 8);
}

void
sis_5513_ide_write(int addr, uint8_t val, void *priv)
{
    sis_5513_ide_t *dev = (sis_5513_ide_t *) priv;

    sis_5513_ide_log("SiS 5513 IDE: [W] dev->pci_conf[%02X] = %02X\n", addr, val);

    switch (addr) {
        case 0x04: /* Command low byte */
            dev->pci_conf[addr] = val & 0x05;
            sis_5513_ide_handler(dev);
            break;
        case 0x06: /* Status low byte */
            dev->pci_conf[addr] = val & 0x20;
            break;
        case 0x07: /* Status high byte */
            dev->pci_conf[addr] = (dev->pci_conf[addr] & 0x06) & ~(val & 0x38);
            break;
        case 0x09: /* Programming Interface Byte */
            switch (dev->rev) {
                case 0xd0:
                    if (dev->sis->ide_bits_1_3_writable)
                        val |= 0x0a;
                    fallthrough;
                case 0x00:
                case 0xd1:
                    val &= 0xbf;
                    fallthrough;
                case 0xc0:
                    switch (val & 0x0a) {
                        case 0x00:
                            dev->pci_conf[addr] = (dev->pci_conf[addr] & 0x85) | (val & 0x4a);
                            break;
                        case 0x02:
                            dev->pci_conf[addr] = (dev->pci_conf[addr] & 0x84) | (val & 0x4b);
                            break;
                        case 0x08:
                            dev->pci_conf[addr] = (dev->pci_conf[addr] & 0x81) | (val & 0x4e);
                            break;
                        case 0x0a:
                            dev->pci_conf[addr] = (dev->pci_conf[addr] & 0x80) | (val & 0x4f);
                            break;
                    }
                    break;
            }
            sis_5513_ide_irq_handler(dev);
            sis_5513_ide_handler(dev);
            break;
        case 0x0d: /* Latency Timer */
            dev->pci_conf[addr] = val;
            break;

        /* Primary Base Address */
        case 0x10 ... 0x11:
        case 0x14 ... 0x15:
            fallthrough;

        /* Secondary Base Address */
        case 0x18 ... 0x19:
        case 0x1c ... 0x1d:
            fallthrough;

        /* Bus Mastering Base Address */
        case 0x20 ... 0x21:
            if (addr == 0x20)
                dev->pci_conf[addr] = (val & 0xe0) | 0x01;
            else if ((addr & 0x07) == 0x00)
                dev->pci_conf[addr] = (val & 0xf8) | 0x01;
            else if ((addr & 0x07) == 0x04)
                dev->pci_conf[addr] = (val & 0xfc) | 0x01;
            else
                dev->pci_conf[addr] = val;
            sis_5513_ide_handler(dev);
            break;

        case 0x2c ... 0x2f:
            if (dev->rev >= 0xd0)
                dev->pci_conf[addr] = val;
            break;

        case 0x30 ... 0x33: /* Expansion ROM Base Address */
#ifdef DATASHEET
            dev->pci_conf[addr] = val;
#else
            if (dev->rev == 0x00)
                dev->pci_conf[addr] = val;
#endif
            break;

        case 0x40: /* IDE Primary Channel/Master Drive Data Recovery Time Control */
            if (dev->rev >= 0xd0)
                dev->pci_conf[addr] = val & 0xcf;
            else
                dev->pci_conf[addr] = val & 0x0f;
            break;

        case 0x42: /* IDE Primary Channel/Slave Drive Data Recovery Time Control */
        case 0x44: /* IDE Secondary Channel/Master Drive Data Recovery Time Control */
        case 0x46: /* IDE Secondary Channel/Slave Drive Data Recovery Time Control */
        case 0x48: /* IDE Command Recovery Time Control */
            dev->pci_conf[addr] = val & 0x0f;
            break;

        case 0x41: /* IDE Primary Channel/Master Drive DataActive Time Control */
        case 0x43: /* IDE Primary Channel/Slave Drive Data Active Time Control */
        case 0x45: /* IDE Secondary Channel/Master Drive Data Active Time Control */
        case 0x47: /* IDE Secondary Channel/Slave Drive Data Active Time Control */
            if (dev->rev >= 0xd0)
                dev->pci_conf[addr] = val & 0xe7;
            else
                dev->pci_conf[addr] = val & 0x07;
            break;

        case 0x49: /* IDE Command Active Time Control */
            dev->pci_conf[addr] = val & 0x07;
            break;

        case 0x4a: /* IDE General Control Register 0 */
            switch (dev->rev) {
                case 0x00:
                   dev->pci_conf[addr] = val & 0x9e;
                   break;
                case 0xc0:
                   dev->pci_conf[addr] = val & 0xaf;
                   break;
                case 0xd0:
                   dev->pci_conf[addr] = val;
                   break;
            }
            sis_5513_ide_handler(dev);
            break;

        case 0x4b: /* IDE General Control Register 1 */
            if (dev->rev >= 0xc0)
                dev->pci_conf[addr] = val;
            else
                dev->pci_conf[addr] = val & 0xef;
            break;

        case 0x4c: /* Prefetch Count of Primary Channel (Low Byte) */
        case 0x4d: /* Prefetch Count of Primary Channel (High Byte) */
        case 0x4e: /* Prefetch Count of Secondary Channel (Low Byte) */
        case 0x4f: /* Prefetch Count of Secondary Channel (High Byte) */
            dev->pci_conf[addr] = val;
            break;

        case 0x50:
        case 0x51:
            if (dev->rev >= 0xd0)
                dev->pci_conf[addr] = val;
            break;

        case 0x52:
            if (dev->rev >= 0xd0)
                dev->pci_conf[addr] = val & 0x0f;
            break;

        default:
            break;
    }
}

uint8_t
sis_5513_ide_read(int addr, void *priv)
{
    const sis_5513_ide_t *dev = (sis_5513_ide_t *) priv;
    uint8_t ret = 0xff;

    switch (addr) {
        default:
            ret = dev->pci_conf[addr];
            break;
        case 0x09:
            ret = dev->pci_conf[addr];
            if (dev->rev >= 0xc0) {
                if (dev->pci_conf[0x09] & 0x40)
                    ret |= ((dev->pci_conf[0x4a] & 0x06) << 3);
                if ((dev->rev == 0xd0) && dev->sis->ide_bits_1_3_writable)
                    ret |= 0x0a;
            }
            break;
        case 0x3d:
            if (dev->rev >= 0xc0)
                ret = (dev->pci_conf[0x09] & 0x05) ? PCI_INTA : 0x00;
            else
                ret = (((dev->pci_conf[0x4b] & 0xc0) == 0xc0) ||
                       (dev->pci_conf[0x09] & 0x05)) ? PCI_INTA : 0x00;
            break;
    }

    sis_5513_ide_log("SiS 5513 IDE: [R] dev->pci_conf[%02X] = %02X\n", addr, ret);

    return ret;
}

static void
sis_5513_ide_reset(void *priv)
{
    sis_5513_ide_t *dev = (sis_5513_ide_t *) priv;

    dev->pci_conf[0x00] = 0x39;
    dev->pci_conf[0x01] = 0x10;
    dev->pci_conf[0x02] = 0x13;
    dev->pci_conf[0x03] = 0x55;
    dev->pci_conf[0x04] = dev->pci_conf[0x05] = 0x00;
    dev->pci_conf[0x06] = dev->pci_conf[0x07] = 0x00;
    dev->pci_conf[0x08] = (dev->rev == 0xd1) ? 0xd0 : dev->rev;
    dev->pci_conf[0x09] = 0x8a;
    dev->pci_conf[0x0a] = dev->pci_conf[0x0b] = 0x01;
    dev->pci_conf[0x0c] = dev->pci_conf[0x0d] = 0x00;
    dev->pci_conf[0x0e] = 0x80;
    dev->pci_conf[0x0f] = 0x00;
    dev->pci_conf[0x10] = 0xf1;
    dev->pci_conf[0x11] = 0x01;
    dev->pci_conf[0x14] = 0xf5;
    dev->pci_conf[0x15] = 0x03;
    dev->pci_conf[0x18] = 0x71;
    dev->pci_conf[0x19] = 0x01;
    dev->pci_conf[0x1c] = 0x75;
    dev->pci_conf[0x1d] = 0x03;
    dev->pci_conf[0x20] = 0x01;
    dev->pci_conf[0x21] = 0xf0;
    dev->pci_conf[0x22] = dev->pci_conf[0x23] = 0x00;
    dev->pci_conf[0x24] = dev->pci_conf[0x25] = 0x00;
    dev->pci_conf[0x26] = dev->pci_conf[0x27] = 0x00;
    dev->pci_conf[0x28] = dev->pci_conf[0x29] = 0x00;
    dev->pci_conf[0x2a] = dev->pci_conf[0x2b] = 0x00;
    switch (dev->rev) {
        case 0x00:
        case 0xd0:
        case 0xd1:
            dev->pci_conf[0x2c] = dev->pci_conf[0x2d] = 0x00;
            break;
        case 0xc0:
#ifdef DATASHEET
            dev->pci_conf[0x2c] = dev->pci_conf[0x2d] = 0x00;
#else
            /* The only Linux lspci listing I could find of this chipset,
               shows a subsystem of 0058:0000. */
            dev->pci_conf[0x2c] = 0x58;
            dev->pci_conf[0x2d] = 0x00;
#endif
            break;
    }
    dev->pci_conf[0x2e] = dev->pci_conf[0x2f] = 0x00;
    dev->pci_conf[0x30] = dev->pci_conf[0x31] = 0x00;
    dev->pci_conf[0x32] = dev->pci_conf[0x33] = 0x00;
    dev->pci_conf[0x40] = dev->pci_conf[0x41] = 0x00;
    dev->pci_conf[0x42] = dev->pci_conf[0x43] = 0x00;
    dev->pci_conf[0x44] = dev->pci_conf[0x45] = 0x00;
    dev->pci_conf[0x46] = dev->pci_conf[0x47] = 0x00;
    dev->pci_conf[0x48] = dev->pci_conf[0x49] = 0x00;
    dev->pci_conf[0x4a] = 0x06;
    dev->pci_conf[0x4b] = 0x00;
    dev->pci_conf[0x4c] = dev->pci_conf[0x4d] = 0x00;
    dev->pci_conf[0x4e] = dev->pci_conf[0x4f] = 0x00;

    sis_5513_ide_irq_handler(dev);
    sis_5513_ide_handler(dev);

    sff_bus_master_reset(dev->sis->bm[0]);
    sff_bus_master_reset(dev->sis->bm[1]);
}

static void
sis_5513_ide_close(void *priv)
{
    sis_5513_ide_t *dev = (sis_5513_ide_t *) priv;

    free(dev);
}

static void *
sis_5513_ide_init(UNUSED(const device_t *info))
{
    sis_5513_ide_t *dev = (sis_5513_ide_t *) calloc(1, sizeof(sis_5513_ide_t));

    dev->rev = info->local;

    dev->sis = device_get_common_priv();

    /* SFF IDE */
    dev->sis->bm[0] = device_add_inst(&sff8038i_device, 1);
    dev->sis->bm[1] = device_add_inst(&sff8038i_device, 2);

    sis_5513_ide_reset(dev);

    return dev;
}

const device_t sis_5513_ide_device = {
    .name          = "SiS 5513 IDE controller",
    .internal_name = "sis_5513_ide",
    .flags         = DEVICE_PCI,
    .local         = 0x00,
    .init          = sis_5513_ide_init,
    .close         = sis_5513_ide_close,
    .reset         = sis_5513_ide_reset,
    { .available = NULL },
    .speed_changed = NULL,
    .force_redraw  = NULL,
    .config        = NULL
};

const device_t sis_5572_ide_device = {
    .name          = "SiS 5572 IDE controller",
    .internal_name = "sis_5572_ide",
    .flags         = DEVICE_PCI,
    .local         = 0xc0,
    .init          = sis_5513_ide_init,
    .close         = sis_5513_ide_close,
    .reset         = sis_5513_ide_reset,
    { .available = NULL },
    .speed_changed = NULL,
    .force_redraw  = NULL,
    .config        = NULL
};

const device_t sis_5582_ide_device = {
    .name          = "SiS 5582 IDE controller",
    .internal_name = "sis_5582_ide",
    .flags         = DEVICE_PCI,
    .local         = 0xd0,
    .init          = sis_5513_ide_init,
    .close         = sis_5513_ide_close,
    .reset         = sis_5513_ide_reset,
    { .available = NULL },
    .speed_changed = NULL,
    .force_redraw  = NULL,
    .config        = NULL
};

const device_t sis_5591_5600_ide_device = {
    .name          = "SiS 5591/(5)600 IDE controller",
    .internal_name = "sis_5591_5600_ide",
    .flags         = DEVICE_PCI,
    .local         = 0xd1,    /* D0, but we need to distinguish them. */
    .init          = sis_5513_ide_init,
    .close         = sis_5513_ide_close,
    .reset         = sis_5513_ide_reset,
    { .available = NULL },
    .speed_changed = NULL,
    .force_redraw  = NULL,
    .config        = NULL
};
