/*
 * 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 OPTi 82C611/611A VLB IDE controller.

 * Authors: Miran Grca, <mgrca8@gmail.com>
 *
 *          Copyright 2020 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/timer.h>
#include <86box/io.h>
#include <86box/device.h>
#include <86box/keyboard.h>
#include <86box/mem.h>
#include <86box/hdc.h>
#include <86box/hdc_ide.h>
#include <86box/plat_unused.h>

typedef struct opti611_t {
    uint8_t is_sec;
    uint8_t tries;
    uint8_t in_cfg;
    uint8_t cfg_locked;
    uint8_t regs[19];
} opti611_t;

static void opti611_ide_handler(opti611_t *dev);

static void
opti611_cfg_write(uint16_t addr, uint8_t val, void *priv)
{
    opti611_t *dev = (opti611_t *) priv;

    addr &= 0x0007;

    switch (addr) {
        case 0x0000:
        case 0x0001:
            dev->regs[((dev->regs[0x06] & 0x01) << 4) + addr] = val;
            break;
        case 0x0002:
            dev->regs[0x12] = (val & 0xc1) | 0x02;
            if (val & 0xc0) {
                if (val & 0x40)
                    dev->cfg_locked = 1;
                dev->in_cfg = 0;
                opti611_ide_handler(dev);
            }
            break;
        case 0x0003:
            dev->regs[0x03] = (val & 0xdf);
            break;
        case 0x0005:
            dev->regs[0x05] = (dev->regs[0x05] & 0x78) | (val & 0x87);
            break;
        case 0x0006:
            dev->regs[0x06] = val;
            break;

        default:
            break;
    }
}

static void
opti611_cfg_writew(uint16_t addr, uint16_t val, void *priv)
{
    opti611_cfg_write(addr, val & 0xff, priv);
    opti611_cfg_write(addr + 1, val >> 8, priv);
}

static void
opti611_cfg_writel(uint16_t addr, uint32_t val, void *priv)
{
    opti611_cfg_writew(addr, val & 0xffff, priv);
    opti611_cfg_writew(addr + 2, val >> 16, priv);
}

static uint8_t
opti611_cfg_read(uint16_t addr, void *priv)
{
    uint8_t          ret = 0xff;
    const opti611_t *dev = (opti611_t *) priv;

    addr &= 0x0007;

    switch (addr) {
        case 0x0000:
        case 0x0001:
            ret = dev->regs[((dev->regs[0x06] & 0x01) << 4) + addr];
            break;
        case 0x0002:
            ret = ((!!in_smm) << 7);
            if (ret & 0x80)
                ret |= (dev->regs[addr] & 0x7f);
            break;
        case 0x0003:
        case 0x0004:
        case 0x0005:
        case 0x0006:
            ret = dev->regs[addr];
            break;

        default:
            break;
    }

    return ret;
}

static uint16_t
opti611_cfg_readw(uint16_t addr, void *priv)
{
    uint16_t ret = 0xffff;

    ret = opti611_cfg_read(addr, priv);
    ret |= (opti611_cfg_read(addr + 1, priv) << 8);

    return ret;
}

static uint32_t
opti611_cfg_readl(uint16_t addr, void *priv)
{
    uint32_t ret = 0xffffffff;

    ret = opti611_cfg_readw(addr, priv);
    ret |= (opti611_cfg_readw(addr + 2, priv) << 16);

    return ret;
}

static void
opti611_ide_write(uint16_t addr, uint8_t val, void *priv)
{
    opti611_t *dev = (opti611_t *) priv;

    uint8_t smia9 = (!!(addr & 0x0200)) << 5;
    uint8_t smia2 = (!!(addr & 0x0004)) << 4;
    uint8_t smibe = (addr & 0x0003);

    if (dev->regs[0x03] & 0x02) {
        smi_raise();
        dev->regs[0x02] = smia9 | smia2 | smibe;
        dev->regs[0x04] = val;
    }
}

static void
opti611_ide_writew(uint16_t addr, UNUSED(uint16_t val), void *priv)
{
    opti611_t *dev = (opti611_t *) priv;

    uint8_t smia9 = (!!(addr & 0x0200)) << 5;
    uint8_t smia2 = (!!(addr & 0x0004)) << 4;
    uint8_t smibe = (addr & 0x0002) | 0x0001;

    if (dev->regs[0x03] & 0x02) {
        smi_raise();
        dev->regs[0x02] = smia9 | smia2 | smibe;
        dev->regs[0x04] = 0x00;
    }
}

static void
opti611_ide_writel(uint16_t addr, UNUSED(uint32_t val), void *priv)
{
    opti611_t *dev = (opti611_t *) priv;

    uint8_t smia9 = (!!(addr & 0x0200)) << 5;
    uint8_t smia2 = (!!(addr & 0x0004)) << 4;

    if (dev->regs[0x03] & 0x02) {
        smi_raise();
        dev->regs[0x02] = smia9 | smia2 | 0x0003;
        dev->regs[0x04] = 0x00;
    }
}

static uint8_t
opti611_ide_read(uint16_t addr, void *priv)
{
    opti611_t *dev = (opti611_t *) priv;

    uint8_t smia9 = (!!(addr & 0x0200)) << 5;
    uint8_t smia2 = (!!(addr & 0x0004)) << 4;
    uint8_t smibe = (addr & 0x0003);

    if (dev->regs[0x03] & 0x02) {
        smi_raise();
        dev->regs[0x02] = smia9 | smia2 | smibe;
        dev->regs[0x04] = 0x00;
    }

    return 0xff;
}

static uint16_t
opti611_ide_readw(uint16_t addr, void *priv)
{
    opti611_t *dev = (opti611_t *) priv;

    uint8_t smia9 = (!!(addr & 0x0200)) << 5;
    uint8_t smia2 = (!!(addr & 0x0004)) << 4;
    uint8_t smibe = (addr & 0x0002) | 0x0001;

    if ((addr & 0x0007) == 0x0001) {
        dev->tries = (dev->tries + 1) & 0x01;
        if ((dev->tries == 0x00) && !dev->cfg_locked) {
            dev->in_cfg = 1;
            opti611_ide_handler(dev);
        }
    }

    if (dev->regs[0x03] & 0x02) {
        smi_raise();
        dev->regs[0x02] = smia9 | smia2 | smibe;
        dev->regs[0x04] = 0x00;
    }

    return 0xffff;
}

static uint32_t
opti611_ide_readl(uint16_t addr, void *priv)
{
    opti611_t *dev = (opti611_t *) priv;

    uint8_t smia9 = (!!(addr & 0x0200)) << 5;
    uint8_t smia2 = (!!(addr & 0x0004)) << 4;

    if (dev->regs[0x03] & 0x02) {
        smi_raise();
        dev->regs[0x02] = smia9 | smia2 | 0x0003;
        dev->regs[0x04] = 0x00;
    }

    return 0xffffffff;
}

static void
opti611_ide_handler(opti611_t *dev)
{
    if (dev->is_sec) {
        ide_sec_disable();
        io_removehandler(0x0170, 0x0007,
                         opti611_ide_read, opti611_ide_readw, opti611_ide_readl,
                         opti611_ide_write, opti611_ide_writew, opti611_ide_writel,
                         dev);
        io_removehandler(0x0170, 0x0007,
                         opti611_cfg_read, opti611_cfg_readw, opti611_cfg_readl,
                         opti611_cfg_write, opti611_cfg_writew, opti611_cfg_writel,
                         dev);

        if (dev->in_cfg && !dev->cfg_locked) {
            io_sethandler(0x0170, 0x0007,
                          opti611_cfg_read, opti611_cfg_readw, opti611_cfg_readl,
                          opti611_cfg_write, opti611_cfg_writew, opti611_cfg_writel,
                          dev);
        } else {
            if (dev->regs[0x03] & 0x01)
                ide_sec_enable();
            io_sethandler(0x0170, 0x0007,
                          opti611_ide_read, opti611_ide_readw, opti611_ide_readl,
                          opti611_ide_write, opti611_ide_writew, opti611_ide_writel,
                          dev);
        }
    } else {
        ide_pri_disable();
        io_removehandler(0x01f0, 0x0007,
                         opti611_ide_read, opti611_ide_readw, opti611_ide_readl,
                         opti611_ide_write, opti611_ide_writew, opti611_ide_writel,
                         dev);
        io_removehandler(0x01f0, 0x0007,
                         opti611_cfg_read, opti611_cfg_readw, opti611_cfg_readl,
                         opti611_cfg_write, opti611_cfg_writew, opti611_cfg_writel,
                         dev);

        if (dev->in_cfg && !dev->cfg_locked) {
            io_sethandler(0x01f0, 0x0007,
                          opti611_cfg_read, opti611_cfg_readw, opti611_cfg_readl,
                          opti611_cfg_write, opti611_cfg_writew, opti611_cfg_writel,
                          dev);
        } else {
            if (dev->regs[0x03] & 0x01)
                ide_pri_enable();
            io_sethandler(0x01f0, 0x0007,
                          opti611_ide_read, opti611_ide_readw, opti611_ide_readl,
                          opti611_ide_write, opti611_ide_writew, opti611_ide_writel,
                          dev);
        }
    }
}

static void
opti611_close(void *priv)
{
    opti611_t *dev = (opti611_t *) priv;

    free(dev);
}

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

    dev->is_sec = info->local;

    dev->regs[0x12] = 0x80;
    dev->regs[0x03] = 0x01;
    dev->regs[0x05] = 0x20;

    device_add(&ide_vlb_device);

    opti611_ide_handler(dev);

    return dev;
}

const device_t ide_opti611_vlb_device = {
    .name          = "OPTi 82C611/82C611A VLB",
    .internal_name = "ide_opti611_vlb",
    .flags         = DEVICE_VLB,
    .local         = 0,
    .init          = opti611_init,
    .close         = opti611_close,
    .reset         = NULL,
    .available     = NULL,
    .speed_changed = NULL,
    .force_redraw  = NULL,
    .config        = NULL
};

const device_t ide_opti611_vlb_sec_device = {
    .name          = "OPTi 82C611/82C611A VLB (Secondary)",
    .internal_name = "ide_opti611_vlb",
    .flags         = DEVICE_VLB,
    .local         = 1,
    .init          = opti611_init,
    .close         = opti611_close,
    .reset         = NULL,
    .available     = NULL,
    .speed_changed = NULL,
    .force_redraw  = NULL,
    .config        = NULL
};
