/*
 * 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 VIA VT82C49X chipset.
 *
 * Authors: Tiseno100,
 *          Miran Grca, <mgrca8@gmail.com>
 *
 *          Copyright 2020 Tiseno100.
 *          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/io.h>
#include <86box/device.h>
#include <86box/mem.h>
#include <86box/smram.h>
#include <86box/pic.h>
#include <86box/timer.h>
#include <86box/hdc.h>
#include <86box/hdc_ide.h>
#include <86box/port_92.h>
#include <86box/chipset.h>

typedef struct vt82c49x_t {
    uint8_t has_ide;
    uint8_t index;
    uint8_t regs[256];

    smram_t *smram_smm;
    smram_t *smram_low;
    smram_t *smram_high;
} vt82c49x_t;

#ifdef ENABLE_VT82C49X_LOG
int vt82c49x_do_log = ENABLE_VT82C49X_LOG;

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

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

static void
vt82c49x_recalc(vt82c49x_t *dev)
{
    int      relocate;
    uint8_t  reg;
    uint8_t  bit;
    uint32_t base;
    uint32_t state;
    uint32_t shadow_bitmap = 0x00000000;

    relocate = (dev->regs[0x33] >> 2) & 0x03;

    shadowbios       = 0;
    shadowbios_write = 0;

    for (uint8_t i = 0; i < 8; i++) {
        base = 0xc0000 + (i << 14);
        reg  = 0x30 + (i >> 2);
        bit  = (i & 3) << 1;

        if ((base >= 0xc0000) && (base <= 0xc7fff)) {
            if (dev->regs[0x40] & 0x80)
                state = MEM_WRITE_DISABLED;
            else if ((dev->regs[reg]) & (1 << bit))
                state = MEM_WRITE_INTERNAL;
            else
                state = (dev->regs[0x33] & 0x40) ? MEM_WRITE_ROMCS : MEM_WRITE_EXTERNAL;

            if ((dev->regs[reg]) & (1 << (bit + 1)))
                state |= MEM_READ_INTERNAL;
            else
                state |= (dev->regs[0x33] & 0x40) ? MEM_READ_ROMCS : MEM_READ_EXTERNAL;
        }
        if ((base >= 0xc8000) && (base <= 0xcffff)) {
            if ((dev->regs[reg]) & (1 << bit))
                state = MEM_WRITE_INTERNAL;
            else
                state = (dev->regs[0x33] & 0x80) ? MEM_WRITE_ROMCS : MEM_WRITE_EXTERNAL;

            if ((dev->regs[reg]) & (1 << (bit + 1)))
                state |= MEM_READ_INTERNAL;
            else
                state |= (dev->regs[0x33] & 0x80) ? MEM_READ_ROMCS : MEM_READ_EXTERNAL;
        } else {
            state = ((dev->regs[reg]) & (1 << bit)) ? MEM_WRITE_INTERNAL : MEM_WRITE_EXTANY;
            state |= ((dev->regs[reg]) & (1 << (bit + 1))) ? MEM_READ_INTERNAL : MEM_READ_EXTANY;
        }

        vt82c49x_log("(%02X=%02X, %i) Setting %08X-%08X to: write %sabled, read %sabled\n",
                     reg, dev->regs[reg], bit, base, base + 0x3fff,
                     ((dev->regs[reg]) & (1 << bit)) ? "en" : "dis", ((dev->regs[reg]) & (1 << (bit + 1))) ? "en" : "dis");

        if ((dev->regs[reg]) & (1 << bit))
            shadow_bitmap |= (1 << i);
        if ((dev->regs[reg]) & (1 << (bit + 1)))
            shadow_bitmap |= (1 << (i + 16));

        mem_set_mem_state_both(base, 0x4000, state);
    }

    for (uint8_t i = 0; i < 4; i++) {
        base = 0xe0000 + (i << 15);
        bit  = 6 - (i & 2);

        if ((base >= 0xe0000) && (base <= 0xe7fff)) {
            if (dev->regs[0x40] & 0x20)
                state = MEM_WRITE_DISABLED;
            else if ((dev->regs[0x32]) & (1 << bit))
                state = MEM_WRITE_INTERNAL;
            else
                state = (dev->regs[0x33] & 0x10) ? MEM_WRITE_ROMCS : MEM_WRITE_EXTERNAL;

            if ((dev->regs[0x32]) & (1 << (bit + 1)))
                state |= MEM_READ_INTERNAL;
            else
                state |= (dev->regs[0x33] & 0x10) ? MEM_READ_ROMCS : MEM_READ_EXTERNAL;
        } else if ((base >= 0xe8000) && (base <= 0xeffff)) {
            if (dev->regs[0x40] & 0x20)
                state = MEM_WRITE_DISABLED;
            else if ((dev->regs[0x32]) & (1 << bit))
                state = MEM_WRITE_INTERNAL;
            else
                state = (dev->regs[0x33] & 0x20) ? MEM_WRITE_ROMCS : MEM_WRITE_EXTERNAL;

            if ((dev->regs[0x32]) & (1 << (bit + 1)))
                state |= MEM_READ_INTERNAL;
            else
                state |= (dev->regs[0x33] & 0x20) ? MEM_READ_ROMCS : MEM_READ_EXTERNAL;
        } else {
            if (dev->regs[0x40] & 0x40)
                state = MEM_WRITE_DISABLED;
            else if ((dev->regs[0x32]) & (1 << bit))
                state = ((dev->regs[0x32]) & (1 << bit)) ? MEM_WRITE_INTERNAL : MEM_WRITE_EXTANY;

            state |= ((dev->regs[0x32]) & (1 << (bit + 1))) ? MEM_READ_INTERNAL : MEM_READ_EXTANY;
        }

        vt82c49x_log("(32=%02X, %i) Setting %08X-%08X to: write %sabled, read %sabled\n",
                     dev->regs[0x32], bit, base, base + 0x7fff,
                     ((dev->regs[0x32]) & (1 << bit)) ? "en" : "dis", ((dev->regs[0x32]) & (1 << (bit + 1))) ? "en" : "dis");

        if ((dev->regs[0x32]) & (1 << bit)) {
            shadow_bitmap |= (0xf << ((i << 2) + 8));
            shadowbios_write |= 1;
        }
        if ((dev->regs[0x32]) & (1 << (bit + 1))) {
            shadow_bitmap |= (0xf << ((i << 2) + 24));
            shadowbios |= 1;
        }

        mem_set_mem_state_both(base, 0x8000, state);
    }

    vt82c49x_log("Shadow bitmap: %08X\n", shadow_bitmap);

    mem_remap_top(0);

    switch (relocate) {
        case 0x02:
            if (!(shadow_bitmap & 0xfff0fff0))
                mem_remap_top(256);
            break;
        case 0x03:
            if (!shadow_bitmap)
                mem_remap_top(384);
            break;
        default:
            break;
    }

    flushmmucache_nopc();
}

static void
vt82c49x_write(uint16_t addr, uint8_t val, void *priv)
{
    vt82c49x_t *dev = (vt82c49x_t *) priv;
    uint8_t     valxor;

    switch (addr) {
        case 0xa8:
            dev->index = val;
            break;

        case 0xa9:
            valxor = (val ^ dev->regs[dev->index]);
            if (dev->index == 0x55)
                dev->regs[dev->index] &= ~val;
            else
                dev->regs[dev->index] = val;

            vt82c49x_log("dev->regs[0x%02x] = %02x\n", dev->index, val);

            switch (dev->index) {
                /* Wait States */
                case 0x03:
                    cpu_update_waitstates();
                    break;

                /* Shadow RAM and top of RAM relocation */
                case 0x30:
                case 0x31:
                case 0x32:
                case 0x33:
                case 0x40:
                    vt82c49x_recalc(dev);
                    break;

                /* External Cache Enable(Based on the 486-VC-HD BIOS) */
                case 0x50:
                    cpu_cache_ext_enabled = (val & 0x84);
                    break;

                /* Software SMI */
                case 0x54:
                    if ((dev->regs[0x5b] & 0x80) && (valxor & 0x01) && (val & 0x01)) {
                        if (dev->regs[0x5b] & 0x20)
                            smi_raise();
                        else
                            picint(1 << 15);
                        dev->regs[0x55] = 0x01;
                    }
                    break;

                /* SMRAM */
                case 0x5b:
                    smram_disable_all();

                    if (val & 0x80) {
                        smram_enable(dev->smram_smm, (val & 0x40) ? 0x00060000 : 0x00030000, 0x000a0000, 0x00020000,
                                     0, (val & 0x10));
                        smram_enable(dev->smram_high, 0x000a0000, 0x000a0000, 0x00020000,
                                     (val & 0x08), (val & 0x08));
                        smram_enable(dev->smram_low, 0x00030000, 0x000a0000, 0x00020000,
                                     (val & 0x02), 0);
                    }
                    break;

                /* Edge/Level IRQ Control */
                case 0x62:
                case 0x63:
                    if (dev->index == 0x63)
                        pic_elcr_write(dev->index, val & 0xde, &pic2);
                    else {
                        pic_elcr_write(dev->index, val & 0xf8, &pic);
                        pic_elcr_set_enabled(val & 0x01);
                    }
                    break;

                /* Local Bus IDE Controller */
                case 0x71:
                    if (dev->has_ide) {
                        ide_pri_disable();
                        ide_set_base(0, (val & 0x40) ? HDC_SECONDARY_BASE : HDC_PRIMARY_BASE);
                        ide_set_side(0, (val & 0x40) ? HDC_SECONDARY_SIDE : HDC_PRIMARY_SIDE);
                        if (val & 0x01)
                            ide_pri_enable();
                        vt82c49x_log("VT82C496 IDE now %sabled as %sary\n", (val & 0x01) ? "en" : "dis",
                                     (val & 0x40) ? "second" : "prim");
                    }
                    break;

                default:
                    break;
            }
            break;

        default:
            break;
    }
}

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

    switch (addr) {
        case 0xa9:
            /* Register 64h is jumper readout. */
            if (dev->index == 0x64)
                ret = 0xff;
            else if (dev->index == 0x63)
                ret = pic_elcr_read(dev->index, &pic2) | (dev->regs[dev->index] & 0x01);
            else if (dev->index == 0x62)
                ret = pic_elcr_read(dev->index, &pic) | (dev->regs[dev->index] & 0x07);
            else if (dev->index < 0x80)
                ret = dev->regs[dev->index];
            break;

        default:
            break;
    }

    return ret;
}

static void
vt82c49x_reset(void *priv)
{
    for (uint16_t i = 0; i < 256; i++)
        vt82c49x_write(i, 0x00, priv);
}

static void
vt82c49x_close(void *priv)
{
    vt82c49x_t *dev = (vt82c49x_t *) priv;

    smram_del(dev->smram_high);
    smram_del(dev->smram_low);
    smram_del(dev->smram_smm);

    free(dev);
}

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

    dev->smram_smm  = smram_add();
    dev->smram_low  = smram_add();
    dev->smram_high = smram_add();

    dev->has_ide = info->local & 1;
    if (dev->has_ide) {
        device_add(&ide_vlb_2ch_device);
        ide_sec_disable();
    }

    device_add(&port_92_device);

    io_sethandler(0x0a8, 0x0002, vt82c49x_read, NULL, NULL, vt82c49x_write, NULL, NULL, dev);

    pic_elcr_io_handler(0);
    pic_elcr_set_enabled(1);

    vt82c49x_recalc(dev);

    return dev;
}

const device_t via_vt82c49x_device = {
    .name          = "VIA VT82C49X",
    .internal_name = "via_vt82c49x",
    .flags         = 0,
    .local         = 0,
    .init          = vt82c49x_init,
    .close         = vt82c49x_close,
    .reset         = NULL,
    .available     = NULL,
    .speed_changed = NULL,
    .force_redraw  = NULL,
    .config        = NULL
};

const device_t via_vt82c49x_pci_device = {
    .name          = "VIA VT82C49X PCI",
    .internal_name = "via_vt82c49x_pci",
    .flags         = DEVICE_PCI,
    .local         = 0,
    .init          = vt82c49x_init,
    .close         = vt82c49x_close,
    .reset         = vt82c49x_reset,
    .available     = NULL,
    .speed_changed = NULL,
    .force_redraw  = NULL,
    .config        = NULL
};

const device_t via_vt82c49x_ide_device = {
    .name          = "VIA VT82C49X (With IDE)",
    .internal_name = "via_vt82c49x_ide",
    .flags         = 0,
    .local         = 1,
    .init          = vt82c49x_init,
    .close         = vt82c49x_close,
    .reset         = NULL,
    .available     = NULL,
    .speed_changed = NULL,
    .force_redraw  = NULL,
    .config        = NULL
};

const device_t via_vt82c49x_pci_ide_device = {
    .name          = "VIA VT82C49X PCI (With IDE)",
    .internal_name = "via_vt82c49x_pci_ide",
    .flags         = DEVICE_PCI,
    .local         = 1,
    .init          = vt82c49x_init,
    .close         = vt82c49x_close,
    .reset         = vt82c49x_reset,
    .available     = NULL,
    .speed_changed = NULL,
    .force_redraw  = NULL,
    .config        = NULL
};
