/*
 * 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.
 *
 *          Crystal CS423x (SBPro/WSS compatible sound chips) emulation.
 *
 * Authors: RichardG, <richardg867@gmail.com>
 *
 *          Copyright 2021-2025 RichardG.
 */
#include <math.h>
#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 <86box/device.h>
#include <86box/dma.h>
#include <86box/gameport.h>
#include <86box/i2c.h>
#include <86box/io.h>
#include <86box/isapnp.h>
#include <86box/midi.h>
#include <86box/timer.h>
#include <86box/mem.h>
#include <86box/nvr.h>
#include <86box/rom.h>
#include <86box/pic.h>
#include <86box/sound.h>
#include <86box/snd_ad1848.h>
#include <86box/snd_opl.h>
#include <86box/snd_sb.h>
#include <86box/plat_fallthrough.h>
#include <86box/plat_unused.h>

#define PNP_ROM_CS4236B      "roms/sound/crystal/PNPISA01.BIN"

#define CRYSTAL_NOEEPROM 0x100

enum {
    CRYSTAL_CS4232  = 0x32, /* no chip ID; dummy value */
    CRYSTAL_CS4236  = 0x36, /* no chip ID; dummy value */
    CRYSTAL_CS4236B = 0xab, /* report an older revision ID to make the values nice and incremental */
    CRYSTAL_CS4237B = 0xc8,
    CRYSTAL_CS4238B = 0xc9,
    CRYSTAL_CS4235  = 0xdd,
    CRYSTAL_CS4239  = 0xde
};
enum {
    CRYSTAL_RAM_CMD     = 0,
    CRYSTAL_RAM_ADDR_LO = 1,
    CRYSTAL_RAM_ADDR_HI = 2,
    CRYSTAL_RAM_DATA    = 3
};
enum {
    CRYSTAL_SLAM_NONE  = 0,
    CRYSTAL_SLAM_INDEX = 1,
    CRYSTAL_SLAM_BYTE1 = 2,
    CRYSTAL_SLAM_BYTE2 = 3
};

#ifdef ENABLE_CS423X_LOG
int cs423x_do_log = ENABLE_CS423X_LOG;

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

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

static const uint8_t slam_init_key[32] = { 0x96, 0x35, 0x9A, 0xCD, 0xE6, 0xF3, 0x79, 0xBC,
                                           0x5E, 0xAF, 0x57, 0x2B, 0x15, 0x8A, 0xC5, 0xE2,
                                           0xF1, 0xF8, 0x7C, 0x3E, 0x9F, 0x4F, 0x27, 0x13,
                                           0x09, 0x84, 0x42, 0xA1, 0xD0, 0x68, 0x34, 0x1A };
static const uint8_t cs4236_default[] = {
    // clang-format off
    /* Chip configuration */
    0x00, 0x03, /* CD-ROM and modem decode */
    0x80, /* misc. config */
    0x80, /* global config */
    0x0b, /* [code base byte (CS4236B+)] / reserved (CS4236) */
    0x20, 0x04, 0x08, 0x10, 0x80, 0x00, 0x00, /* reserved */
    0x00, /* external decode length */
    0x48, /* reserved */
    0x75, 0xb9, 0xfc, /* IRQ routing */
    0x10, 0x03, /* DMA routing */

    /* Default PnP data */
    0x0e, 0x63, 0x42, 0x36, 0xff, 0xff, 0xff, 0xff, 0x00 /* hinted by documentation to be just the header */
    // clang-format on
};

typedef struct cs423x_t {
    void    *pnp_card;
    ad1848_t ad1848;
    sb_t    *sb;
    void    *gameport;
    void    *i2c;
    void    *eeprom;

    uint16_t wss_base;
    uint16_t opl_base;
    uint16_t sb_base;
    uint16_t ctrl_base;
    uint16_t ram_addr;
    uint16_t eeprom_size : 11;
    uint16_t pnp_offset;
    uint16_t pnp_size;
    uint8_t  type;
    uint8_t  ad1848_type;
    uint8_t  regs[8];
    uint8_t  indirect_regs[16];
    uint8_t  eeprom_data[2048];
    uint8_t  ram_data[65536];
    uint8_t  ram_dl : 2;
    uint8_t  opl_wss : 1;
    char    *nvr_path;

    uint8_t                 pnp_enable : 1;
    uint8_t                 key_pos : 5;
    uint8_t                 slam_enable : 1;
    uint8_t                 slam_state : 2;
    uint8_t                 slam_ld;
    uint8_t                 slam_reg;
    isapnp_device_config_t *slam_config;
} cs423x_t;

static void cs423x_slam_enable(cs423x_t *dev, uint8_t enable);
static void cs423x_ctxswitch_write(uint16_t addr, UNUSED(uint8_t val), void *priv);
static void cs423x_pnp_enable(cs423x_t *dev, uint8_t update_rom, uint8_t update_hwconfig);
static void cs423x_pnp_config_changed(uint8_t ld, isapnp_device_config_t *config, void *priv);
static void cs423x_reset(void *priv);

static void
cs423x_nvram(cs423x_t *dev, uint8_t save)
{
    FILE *fp = nvr_fopen(dev->nvr_path, save ? "wb" : "rb");
    if (fp) {
        if (save)
            fwrite(dev->eeprom_data, sizeof(dev->eeprom_data), 1, fp);
        else
            (void) !fread(dev->eeprom_data, sizeof(dev->eeprom_data), 1, fp);
        fclose(fp);
    } else {
        cs423x_log("CS423x: EEPROM data %s failed\n", save ? "save" : "load");
    }
}

static uint8_t
cs423x_read(uint16_t addr, void *priv)
{
    cs423x_t *dev = (cs423x_t *) priv;
    uint8_t   reg = addr & 7;
    uint8_t   ret = dev->regs[reg];

    switch (reg) {
        case 1: /* EEPROM Interface */
            ret &= ~0x04;
            if ((dev->regs[1] & 0x04) && i2c_gpio_get_sda(dev->i2c))
                ret |= 0x04;
            break;

        case 3: /* Control Indirect Access Register (CS4236B+) */
            /* Intel VS440FX BIOS tells CS4236 from CS4232 through the upper bits. Setting them is enough. */
            if (dev->type >= CRYSTAL_CS4236)
                ret |= 0xf0;
            break;

        case 4: /* Control Indirect Data Register (CS4236B+) / Control Data Register (CS4236) */
            if (dev->type >= CRYSTAL_CS4236B)
                ret = dev->indirect_regs[dev->regs[3]];
            break;

        case 5: /* Control/RAM Access */
            /* Reading RAM is undocumented, but performed by:
               - Windows drivers (unknown purpose)
               - Intel VS440FX BIOS (PnP ROM checksum recalculation) */
            if (dev->ram_dl == CRYSTAL_RAM_DATA) {
                ret = dev->ram_data[dev->ram_addr];
                cs423x_log("CS423x: RAM read(%04X) = %02X\n", dev->ram_addr, ret);
                dev->ram_addr++;
            }
            break;

        case 7: /* Global Status (CS4236+) */
            if (dev->type < CRYSTAL_CS4236)
                break;

            /* Context switching: take active context and interrupt flag, then clear interrupt flag. */
            ret &= 0xc0;
            dev->regs[7] &= 0x80;

            if (dev->sb->mpu->state.irq_pending) /* MPU interrupt */
                ret |= 0x08;
            if (dev->ad1848.status & 0x01) /* WSS interrupt */
                ret |= 0x10;
            if (dev->sb->dsp.sb_irq8 || dev->sb->dsp.sb_irq16 || dev->sb->dsp.sb_irq401) /* SBPro interrupt */
                ret |= 0x20;

            break;

        default:
            break;
    }

    cs423x_log("CS423x: read(%X) = %02X\n", reg, ret);

    return ret;
}

static void
cs423x_write(uint16_t addr, uint8_t val, void *priv)
{
    cs423x_t *dev = (cs423x_t *) priv;
    uint8_t   reg = addr & 7;

    cs423x_log("CS423x: write(%X, %02X)\n", reg, val);

    switch (reg) {
        case 0: /* Joystick and Power Control */
            if (dev->type <= CRYSTAL_CS4232)
                val &= 0xeb;
            if ((dev->type >= CRYSTAL_CS4235) && (addr == 0) && (val & 0x08)) {
                /* CS4235+ through X26 backdoor only (hence the addr check): WSS off (one-way trip?) */
                io_removehandler(dev->wss_base, 4, ad1848_read, NULL, NULL, ad1848_write, NULL, NULL, &dev->ad1848);
                io_removehandler(dev->wss_base, 4, NULL, NULL, NULL, cs423x_ctxswitch_write, NULL, NULL, dev);
                dev->wss_base = 0;
            }
            break;

        case 1: /* EEPROM Interface */
            if (dev->type <= CRYSTAL_CS4232)
                val &= 0x37;
            if (val & 0x04)
                i2c_gpio_set(dev->i2c, val & 0x01, val & 0x02);
            break;

        case 2: /* Block Power Down (CS4236+) */
            if (dev->type < CRYSTAL_CS4236)
                return;
            break;

        case 3: /* Control Indirect Access Register (CS4236B+) */
            if (dev->type < CRYSTAL_CS4236) /* must be writable on CS4236 for the aforementioned VS440FX BIOS check */
                return;
            val &= 0x0f;
            break;

        case 4: /* Control Indirect Data Register (CS4236B+) / Control Data Register (CS4236) */
            if (dev->type < CRYSTAL_CS4236) {
                return;
            } else if (dev->type == CRYSTAL_CS4236) {
                val &= 0x40;
                break;
            }
            switch (dev->regs[3] & 0x0f) {
                case 0: /* WSS Master Control */
                    if ((dev->type < CRYSTAL_CS4235) && (val & 0x80)) {
                        ad1848_init(&dev->ad1848, dev->ad1848_type);
                        ad1848_set_cd_audio_channel(&dev->ad1848, AD1848_AUX2);
                    }
                    val = 0x00;
                    break;

                case 1:         /* Version / Chip ID */
                case 7:         /* Reserved */
                case 10 ... 15: /* unspecified */
                    return;

                case 2: /* 3D Space and {Center|Volume} (CS4237B+) */
                    if (dev->type < CRYSTAL_CS4237B)
                        return;
                    break;

                case 3: /* 3D Enable (CS4237B+) */
                    if (dev->type < CRYSTAL_CS4237B)
                        return;
                    val &= 0xe0;
                    break;

                case 4: /* Consumer Serial Port Enable (CS423[78]B, unused on CS4235+) */
                    if (dev->type < CRYSTAL_CS4237B)
                        return;
                    val &= 0xf0;
                    break;

                case 5: /* Lower Channel Status (CS423[78]B, unused on CS4235+) */
                    if (dev->type < CRYSTAL_CS4237B)
                        return;
                    if (dev->type < CRYSTAL_CS4235) /* bit 0 changed from reserved to unused on CS4235 */
                        val &= 0xfe;
                    break;

                case 6: /* Upper Channel Status (CS423[78]B, unused on CS4235+) */
                    if (dev->type < CRYSTAL_CS4237B)
                        return;
                    break;

                case 8: /* CS9236 Wavetable Control */
                    val &= 0x0f;
                    cs423x_pnp_enable(dev, 0, 0);

                    /* Update WTEN state on the WSS codec. */
                    dev->ad1848.wten = !!(val & 0x08);
                    ad1848_updatevolmask(&dev->ad1848);
                    break;

                case 9: /* Power Management (CS4235+) */
                    if (dev->type < CRYSTAL_CS4235)
                        return;
                    if ((dev->indirect_regs[dev->regs[3]] & 0x80) && !(val & 0x80)) {
                        cs423x_reset(dev);
                        return;
                    }
                    val &= 0x83;
                    break;

                default:
                    break;
            }
            dev->indirect_regs[dev->regs[3]] = val;
            break;

        case 5: /* Control/RAM Access */
            switch (dev->ram_dl) {
                case CRYSTAL_RAM_CMD: /* commands */
                    switch (val) {
                        case 0x55: /* Disable PnP Key */
                            dev->pnp_enable = 0;
                            fallthrough;

                        case 0x5a: /* Update Hardware Configuration Data */
                            cs423x_pnp_enable(dev, 0, 1);
                            break;

                        case 0x56: /* Disable Crystal Key */
                            cs423x_slam_enable(dev, 0);
                            break;

                        case 0x57: /* Jump to ROM */
                            break;

                        case 0xaa: /* Download RAM */
                            dev->ram_dl = CRYSTAL_RAM_ADDR_LO;
                            break;

                        default:
                            break;
                    }
                    break;

                case CRYSTAL_RAM_ADDR_LO: /* low address byte */
                    dev->ram_addr = val;
                    dev->ram_dl = CRYSTAL_RAM_ADDR_HI;
                    break;

                case CRYSTAL_RAM_ADDR_HI: /* high address byte */
                    dev->ram_addr |= val << 8;
                    dev->ram_dl = CRYSTAL_RAM_DATA;
                    cs423x_log("CS423x: RAM start(%04X)\n", dev->ram_addr);
                    break;

                case CRYSTAL_RAM_DATA: /* data */
                    cs423x_log("CS423x: RAM write(%04X, %02X)\n", dev->ram_addr, val);
                    dev->ram_data[dev->ram_addr++] = val;
                    break;

                default:
                    break;
            }
            break;

        case 6: /* RAM Access End */
            /* TriGem Delhi-III BIOS writes undocumented value 0x40 instead of 0x00. */
            if ((val == 0x00) || (val == 0x40)) {
                cs423x_log("CS423x: RAM end\n");
                dev->ram_dl = CRYSTAL_RAM_CMD;

                /* Update PnP state and resource data. */
                dev->pnp_size = (dev->type >= CRYSTAL_CS4236) ? 384 : 256; /* we don't know the length */
                cs423x_pnp_enable(dev, 1, 0);
            }
            break;

        case 7: /* Global Status (CS4236+) */
            return;

        default:
            break;
    }

    dev->regs[reg] = val;
}

static void
cs423x_slam_write(UNUSED(uint16_t addr), uint8_t val, void *priv)
{
    cs423x_t *dev = (cs423x_t *) priv;
    uint8_t   idx;

    if ((dev->slam_state != CRYSTAL_SLAM_NONE) || (val == slam_init_key[dev->key_pos])) /* cut down on ISAPnP-related noise */
        cs423x_log("CS423x: slam_write(%02X)\n", val);

    switch (dev->slam_state) {
        case CRYSTAL_SLAM_NONE:
            /* Not in SLAM: read and compare Crystal key. */
            if (val == slam_init_key[dev->key_pos]) {
                dev->key_pos++;
                /* Was the key successfully written? */
                if (!dev->key_pos) {
                    /* Discard any pending logical device configuration, just to be safe. */
                    if (dev->slam_config) {
                        free(dev->slam_config);
                        dev->slam_config = NULL;
                    }

                    /* Enter SLAM. */
                    cs423x_log("CS423x: SLAM unlocked\n");
                    dev->slam_state = CRYSTAL_SLAM_INDEX;
                }
            } else {
                dev->key_pos = 0;
            }
            break;

        case CRYSTAL_SLAM_INDEX:
            /* Intercept the Activate Audio Device command. */
            if (val == 0x79) {
                cs423x_log("CS423x: Exiting SLAM\n");

                /* Apply the last logical device's configuration. */
                if (dev->slam_config) {
                    cs423x_pnp_config_changed(dev->slam_ld, dev->slam_config, dev);
                    free(dev->slam_config);
                    dev->slam_config = NULL;
                }

                /* Exit out of SLAM. */
                dev->slam_state = CRYSTAL_SLAM_NONE;
                break;
            }

            /* Write register index. */
            dev->slam_reg   = val;
            dev->slam_state = CRYSTAL_SLAM_BYTE1;
            break;

        case CRYSTAL_SLAM_BYTE1:
        case CRYSTAL_SLAM_BYTE2:
            /* Write register value: two bytes for I/O ports, single byte otherwise. */
            cs423x_log("CS423x: SLAM write(%02X, %02X)\n", dev->slam_reg, val);
            switch (dev->slam_reg) {
                case 0x06: /* Card Select Number */
                    isapnp_set_csn(dev->pnp_card, val);
                    break;

                case 0x15: /* Logical Device ID */
                    /* Apply the previous logical device's configuration, and reuse its config structure. */
                    if (dev->slam_config)
                        cs423x_pnp_config_changed(dev->slam_ld, dev->slam_config, dev);
                    else
                        dev->slam_config = (isapnp_device_config_t *) calloc(1, sizeof(isapnp_device_config_t));

                    /* Start new logical device. */
                    dev->slam_ld = val;
                    break;

                case 0x47: /* I/O Port Base Address 0 */
                case 0x48: /* I/O Port Base Address 1 */
                case 0x42: /* I/O Port Base Address 2 */
                    idx = (dev->slam_reg == 0x42) ? 2 : (dev->slam_reg - 0x47);
                    if (dev->slam_state == CRYSTAL_SLAM_BYTE1) {
                        /* Set high byte, or ignore it if no logical device is selected. */
                        if (dev->slam_config)
                            dev->slam_config->io[idx].base = val << 8;

                        /* Prepare for the second (low byte) write. */
                        dev->slam_state = CRYSTAL_SLAM_BYTE2;
                        return;
                    } else if (dev->slam_config) {
                        /* Set low byte, or ignore it if no logical device is selected. */
                        dev->slam_config->io[idx].base |= val;
                    }
                    break;

                case 0x22: /* Interrupt Select 0 */
                case 0x27: /* Interrupt Select 1 */
                    /* Stop if no logical device is selected. */
                    if (!dev->slam_config)
                        break;

                    /* Set IRQ value. */
                    idx                            = (dev->slam_reg == 0x22) ? 0 : 1;
                    dev->slam_config->irq[idx].irq = val & 15;
                    break;

                case 0x2a: /* DMA Select 0 */
                case 0x25: /* DMA Select 1 */
                    /* Stop if no logical device is selected. */
                    if (!dev->slam_config)
                        break;

                    /* Set DMA value. */
                    idx                            = (dev->slam_reg == 0x2a) ? 0 : 1;
                    dev->slam_config->dma[idx].dma = val & 7;
                    break;

                case 0x33: /* Activate Device */
                    /* Stop if no logical device is selected. */
                    if (!dev->slam_config)
                        break;

                    /* Activate or deactivate the device. */
                    dev->slam_config->activate = val & 0x01;
                    break;

                default:
                    break;
            }

            /* Prepare for the next register, unless a two-byte write returns above. */
            dev->slam_state = CRYSTAL_SLAM_INDEX;
            break;

        default:
            break;
    }
}

static void
cs423x_slam_enable(cs423x_t *dev, uint8_t enable)
{
    /* Disable SLAM. */
    if (dev->slam_enable) {
        dev->slam_state  = CRYSTAL_SLAM_NONE;
        dev->slam_enable = 0;
        io_removehandler(0x279, 1, NULL, NULL, NULL, cs423x_slam_write, NULL, NULL, dev);
    }

    /* Enable SLAM if the CKD bit is not set. */
    if (enable && !(dev->ram_data[0x4002] & 0x10)) {
        cs423x_log("CS423x: Enabling SLAM\n");
        dev->slam_enable = 1;
        io_sethandler(0x279, 1, NULL, NULL, NULL, cs423x_slam_write, NULL, NULL, dev);
    } else {
        cs423x_log("CS423x: Disabling SLAM\n");
    }
}

static void
cs423x_ctxswitch_write(uint16_t addr, UNUSED(uint8_t val), void *priv)
{
    cs423x_t *dev        = (cs423x_t *) priv;
    uint8_t   ctx        = (dev->regs[7] & 0x80);
    uint8_t   enable_opl = (dev->ad1848.xregs[4] & 0x10) && !(dev->indirect_regs[2] & 0x85);

    /* Check if a context switch (WSS=1 <-> SBPro=0) occurred through the address being written. */
    if ((dev->regs[7] & 0x80) ? ((addr & 0xfff0) == dev->sb_base) : ((addr & 0xfffc) == dev->wss_base)) {
        /* Flip context bit. */
        dev->regs[7] ^= 0x80;
        ctx ^= 0x80;
        cs423x_log("CS423x: Context switch to %s\n", ctx ? "WSS" : "SBPro");

        /* Update CD audio filter.
           FIXME: not thread-safe: filter function TOCTTOU in sound_cd_thread! */
        sound_set_cd_audio_filter(NULL, NULL);
        if (ctx) /* WSS */
            sound_set_cd_audio_filter(ad1848_filter_cd_audio, &dev->ad1848);
        else /* SBPro */
            sound_set_cd_audio_filter(sbpro_filter_cd_audio, dev->sb);

        /* Fire a context switch interrupt if enabled. */
        if ((dev->regs[0] & 0x20) && (dev->ad1848.irq > 0)) {
            dev->regs[7] |= 0x40;         /* set interrupt flag */
            picint(1 << dev->ad1848.irq); /* control device shares IRQ with WSS and SBPro */
        }
    }

    /* Update OPL ownership and state regardless of context switch,
       to trap writes to other registers which may disable the OPL. */
    dev->sb->opl_enabled = !ctx && enable_opl;
    dev->opl_wss         = ctx && enable_opl;
}

static void
cs423x_get_buffer(int32_t *buffer, int len, void *priv)
{
    cs423x_t       *dev = (cs423x_t *) priv;

    /* Output audio from the WSS codec, and also the OPL if we're in charge of it. */
    ad1848_update(&dev->ad1848);

    /* Don't output anything if the analog section or DAC is powered down. */
    if (!(dev->regs[2] & 0xb4) && !(dev->indirect_regs[9] & 0x04)) {
        for (int c = 0; c < len * 2; c += 2) {
            buffer[c] += dev->ad1848.buffer[c] / 2;
            buffer[c + 1] += dev->ad1848.buffer[c + 1] / 2;
        }
    }

    dev->ad1848.pos = 0;
}

static void
cs423x_get_music_buffer(int32_t *buffer, int len, void *priv)
{
    cs423x_t *dev = (cs423x_t *) priv;

    /* Output audio from the WSS codec, and also the OPL if we're in charge of it. */
    if (dev->opl_wss) {
        const int32_t *opl_buf = dev->sb->opl.update(dev->sb->opl.priv);

        /* Don't output anything if the analog section, DAC (DAC2 instead on CS4235+) or FM synth is powered down. */
        uint8_t bpd_mask = (dev->type >= CRYSTAL_CS4235) ? 0xb1 : 0xb5;
        if (!(dev->regs[2] & bpd_mask) && !(dev->indirect_regs[9] & 0x06)) {
            for (int c = 0; c < len * 2; c += 2) {
                buffer[c] += (opl_buf[c] * dev->ad1848.fm_vol_l) >> 16;
                buffer[c + 1] += (opl_buf[c + 1] * dev->ad1848.fm_vol_r) >> 16;
            }
        }

        dev->sb->opl.reset_buffer(dev->sb->opl.priv);
    }
}

static void
cs423x_pnp_enable(cs423x_t *dev, uint8_t update_rom, uint8_t update_hwconfig)
{
    cs423x_log("CS423x: Updating PnP ROM=%d hwconfig=%d\n", update_rom, update_hwconfig);

    if (dev->pnp_card) {
        /* Update PnP resource data if requested. */
        if (update_rom)
            isapnp_update_card_rom(dev->pnp_card, &dev->ram_data[dev->pnp_offset], dev->pnp_size);

        /* Disable PnP key if the PKD bit is set, or if it was disabled by command 0x55. */
        isapnp_enable_card(dev->pnp_card, ((dev->ram_data[0x4002] & 0x20) || !dev->pnp_enable) ? ISAPNP_CARD_NO_KEY : ISAPNP_CARD_ENABLE);
    }

    /* Update some register bits based on the config data in RAM if requested. */
    if (update_hwconfig) {
        /* Update WTEN. */
        if (dev->ram_data[0x4003] & 0x08) {
            dev->indirect_regs[8] |= 0x08;
            dev->ad1848.wten = 1;
        } else {
            dev->indirect_regs[8] &= ~0x08;
            dev->ad1848.wten = 0;
        }

        /* Update SPS. */
        if ((dev->type >= CRYSTAL_CS4236B) && (dev->type <= CRYSTAL_CS4238B)) {
            if (dev->ram_data[0x4003] & 0x04)
                dev->indirect_regs[8] |= 0x04;
            else
                dev->indirect_regs[8] &= ~0x04;
        }

        /* Update IFM. */
        if (dev->ram_data[0x4003] & 0x80)
            dev->ad1848.xregs[4] |= 0x10;
        else
            dev->ad1848.xregs[4] &= ~0x10;

        if (dev->type == CRYSTAL_CS4236) {
            /* Update VCEN. */
            if (dev->ram_data[0x4002] & 0x04)
                dev->regs[4] |= 0x40;
            else
                dev->regs[4] &= ~0x40;            
        }

        if (dev->type >= CRYSTAL_CS4235) {
            /* Update X18 and X19 values. */
            dev->ad1848.xregs[18] = (dev->ad1848.xregs[18] & ~0x3e) | (dev->ram_data[0x400b] & 0x3e);
            dev->ad1848.xregs[19] = dev->ram_data[0x4005];
        }

        /* Inform WSS codec of the changes. */
        ad1848_updatevolmask(&dev->ad1848);
    }
}

static void
cs423x_pnp_config_changed(uint8_t ld, isapnp_device_config_t *config, void *priv)
{
    cs423x_t *dev = (cs423x_t *) priv;

    switch (ld) {
        case 0: /* WSS, OPL3 and SBPro */
            if (dev->wss_base) {
                io_removehandler(dev->wss_base, 4, ad1848_read, NULL, NULL, ad1848_write, NULL, NULL, &dev->ad1848);
                io_removehandler(dev->wss_base, 4, NULL, NULL, NULL, cs423x_ctxswitch_write, NULL, NULL, dev);
                dev->wss_base = 0;
            }

            if (dev->opl_base) {
                io_removehandler(dev->opl_base, 4, dev->sb->opl.read, NULL, NULL, dev->sb->opl.write, NULL, NULL, dev->sb->opl.priv);
                dev->opl_base = 0;
            }

            if (dev->sb_base) {
                sb_dsp_setaddr(&dev->sb->dsp, 0);
                io_removehandler(dev->sb_base, 4, dev->sb->opl.read, NULL, NULL, dev->sb->opl.write, NULL, NULL, dev->sb->opl.priv);
                io_removehandler(dev->sb_base + 8, 2, dev->sb->opl.read, NULL, NULL, dev->sb->opl.write, NULL, NULL, dev->sb->opl.priv);
                io_removehandler(dev->sb_base + 4, 2, sb_ct1345_mixer_read, NULL, NULL, sb_ct1345_mixer_write, NULL, NULL, dev->sb);
                io_removehandler(dev->sb_base, 16, NULL, NULL, NULL, cs423x_ctxswitch_write, NULL, NULL, dev);
                dev->sb_base = 0;
            }

            ad1848_setirq(&dev->ad1848, 0);
            sb_dsp_setirq(&dev->sb->dsp, 0);

            ad1848_setdma(&dev->ad1848, 0);
            sb_dsp_setdma8(&dev->sb->dsp, 0);

            if (config->activate) {
                if (config->io[0].base != ISAPNP_IO_DISABLED) {
                    dev->wss_base = config->io[0].base;
                    io_sethandler(dev->wss_base, 4, ad1848_read, NULL, NULL, ad1848_write, NULL, NULL, &dev->ad1848);
                    io_sethandler(dev->wss_base, 4, NULL, NULL, NULL, cs423x_ctxswitch_write, NULL, NULL, dev);
                }

                if (config->io[1].base != ISAPNP_IO_DISABLED) {
                    dev->opl_base = config->io[1].base;
                    io_sethandler(dev->opl_base, 4, dev->sb->opl.read, NULL, NULL, dev->sb->opl.write, NULL, NULL, dev->sb->opl.priv);
                }

                if (config->io[2].base != ISAPNP_IO_DISABLED) {
                    dev->sb_base = config->io[2].base;
                    sb_dsp_setaddr(&dev->sb->dsp, dev->sb_base);
                    io_sethandler(dev->sb_base, 4, dev->sb->opl.read, NULL, NULL, dev->sb->opl.write, NULL, NULL, dev->sb->opl.priv);
                    io_sethandler(dev->sb_base + 8, 2, dev->sb->opl.read, NULL, NULL, dev->sb->opl.write, NULL, NULL, dev->sb->opl.priv);
                    io_sethandler(dev->sb_base + 4, 2, sb_ct1345_mixer_read, NULL, NULL, sb_ct1345_mixer_write, NULL, NULL, dev->sb);
                    io_sethandler(dev->sb_base, 16, NULL, NULL, NULL, cs423x_ctxswitch_write, NULL, NULL, dev);
                }

                if (config->irq[0].irq != ISAPNP_IRQ_DISABLED) {
                    ad1848_setirq(&dev->ad1848, config->irq[0].irq);
                    sb_dsp_setirq(&dev->sb->dsp, config->irq[0].irq);
                }

                if (config->dma[0].dma != ISAPNP_DMA_DISABLED) {
                    ad1848_setdma(&dev->ad1848, config->dma[0].dma);
                    sb_dsp_setdma8(&dev->sb->dsp, config->dma[0].dma);
                }
            }
            break;

        case 1: /* Game Port */
            if (dev->gameport)
                gameport_remap(dev->gameport, (config->activate && (config->io[0].base != ISAPNP_IO_DISABLED)) ? config->io[0].base : 0);
            break;

        case 2: /* Control Registers */
            if (dev->ctrl_base) {
                io_removehandler(dev->ctrl_base, 8, cs423x_read, NULL, NULL, cs423x_write, NULL, NULL, dev);
                dev->ctrl_base = 0;
            }

            if (config->activate && (config->io[0].base != ISAPNP_IO_DISABLED)) {
                dev->ctrl_base = config->io[0].base;
                io_sethandler(dev->ctrl_base, 8, cs423x_read, NULL, NULL, cs423x_write, NULL, NULL, dev);
            }

            break;

        case 3: /* MPU-401 */
            mpu401_change_addr(dev->sb->mpu, 0);
            mpu401_setirq(dev->sb->mpu, 0);

            if (config->activate) {
                if (config->io[0].base != ISAPNP_IO_DISABLED)
                    mpu401_change_addr(dev->sb->mpu, config->io[0].base);

                if (config->irq[0].irq != ISAPNP_IRQ_DISABLED)
                    mpu401_setirq(dev->sb->mpu, config->irq[0].irq);
            }

            break;

        default:
            break;
    }
}

static void
cs423x_load_defaults(cs423x_t *dev, uint8_t *dest)
{
    switch (dev->type) {
        case CRYSTAL_CS4236:
        case CRYSTAL_CS4236B:
        case CRYSTAL_CS4237B:
        case CRYSTAL_CS4238B:
        case CRYSTAL_CS4235:
        case CRYSTAL_CS4239:
            memcpy(dest, cs4236_default, sizeof(cs4236_default));
            dev->pnp_size = 9; /* header-only PnP ROM size */

            switch (dev->type) {
                case CRYSTAL_CS4236:
                    dest[4] = 0x43; /* code base byte */
                    break;

                case CRYSTAL_CS4235:
                case CRYSTAL_CS4239:
                    dest[4]  = 0x05; /* code base byte */
                    dest[12] = 0x08; /* external decode length */
                    break;
            }
            break;
    }
}

static void
cs423x_reset(void *priv)
{
    cs423x_t *dev = (cs423x_t *) priv;

    /* Clear RAM. */
    memset(dev->ram_data, 0, sizeof(dev->ram_data));

    /* Load default configuration data to RAM. */
    cs423x_load_defaults(dev, &dev->ram_data[0x4000]);

    if (dev->eeprom) {
        /* Load EEPROM data to RAM if the magic bytes are present. */
        if ((dev->eeprom_data[0] == 0x55) && (dev->eeprom_data[1] == 0xbb)) {
            cs423x_log("CS423x: EEPROM data valid, loading to RAM\n");
            dev->pnp_size = (dev->eeprom_data[2] << 8) | dev->eeprom_data[3];
            if (dev->pnp_size > 384)
                dev->pnp_size = 384;
            memcpy(&dev->ram_data[0x4000], &dev->eeprom_data[4], sizeof(dev->eeprom_data) - 4);
        } else {
            cs423x_log("CS423x: EEPROM data invalid, ignoring\n");
        }

        /* Save EEPROM contents to file. */
        cs423x_nvram(dev, 1);
    }

    /* Reset registers. */
    memset(dev->regs, 0, sizeof(dev->regs));
    dev->regs[1] = 0x80;
    memset(dev->indirect_regs, 0, sizeof(dev->indirect_regs));
    dev->indirect_regs[1] = dev->type;
    if (dev->type == CRYSTAL_CS4238B)
        dev->indirect_regs[2] = 0x20;

    /* Reset WSS codec. */
    ad1848_init(&dev->ad1848, dev->ad1848_type);
    ad1848_set_cd_audio_channel(&dev->ad1848, AD1848_AUX2);

    /* Reset PnP resource data, state and logical devices. */
    dev->pnp_enable = 1;
    cs423x_pnp_enable(dev, 1, 1);
    if (dev->pnp_card && dev->sb)
        isapnp_reset_card(dev->pnp_card);

    /* Reset SLAM. */
    cs423x_slam_enable(dev, 1);
}

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

    /* Initialize model-specific data. */
    dev->type = info->local & 0xff;
    cs423x_log("CS423x: init(%02X)\n", dev->type);
    switch (dev->type) {
        case CRYSTAL_CS4236:
        case CRYSTAL_CS4236B:
        case CRYSTAL_CS4237B:
        case CRYSTAL_CS4238B:
        case CRYSTAL_CS4235:
        case CRYSTAL_CS4239:
            /* Different WSS codec families. */
            dev->ad1848_type = (dev->type >= CRYSTAL_CS4235) ? AD1848_TYPE_CS4235 : ((dev->type >= CRYSTAL_CS4236B) ? AD1848_TYPE_CS4236B : AD1848_TYPE_CS4236);

            /* Different Chip Version and ID values (N/A on CS4236), which shouldn't be reset by ad1848_init. */
            dev->ad1848.xregs[25] = dev->type;

            /* Same EEPROM structure. */
            dev->pnp_offset = 0x4013;

            if (!(info->local & CRYSTAL_NOEEPROM)) {
                /* Start a new EEPROM with the default configuration data. */
                cs423x_load_defaults(dev, &dev->eeprom_data[4]);

                /* Load PnP resource data ROM. */
                FILE *fp = rom_fopen(PNP_ROM_CS4236B, "rb");
                if (fp) {
                    uint16_t eeprom_pnp_offset = (dev->pnp_offset & 0x1ff) + 4;
                    /* This is wrong. The header field only indicates PnP resource data length, and real chips use
                       it to locate the firmware patch area, but we don't need any of that, so we can get away
                       with pretending the whole ROM is PnP data, at least until we can get full EEPROM dumps. */
                    dev->pnp_size = fread(&dev->eeprom_data[eeprom_pnp_offset], 1, sizeof(dev->eeprom_data) - eeprom_pnp_offset, fp);
                    fclose(fp);
                } else {
                    dev->pnp_size = 0;
                }

                /* Populate EEPROM header if the PnP ROM was loaded. */
                if (dev->pnp_size) {
                    dev->eeprom_data[0] = 0x55;
                    dev->eeprom_data[1] = 0xbb;
                    dev->eeprom_data[2] = dev->pnp_size >> 8;
                    dev->eeprom_data[3] = dev->pnp_size;
                }

                /* Patch PnP ROM and set EEPROM file name. */
                switch (dev->type) {
                    case CRYSTAL_CS4236:
                        if (dev->pnp_size) {
                            dev->eeprom_data[26] = 0x36;
                            dev->eeprom_data[45] = ' ';
                        }
                        dev->nvr_path = "cs4236.nvr";
                        break;

                    case CRYSTAL_CS4236B:
                        dev->nvr_path = "cs4236b.nvr";
                        break;

                    case CRYSTAL_CS4237B:
                        if (dev->pnp_size) {
                            dev->eeprom_data[26] = 0x37;
                            dev->eeprom_data[44] = '7';
                        }
                        dev->nvr_path = "cs4237b.nvr";
                        break;

                    case CRYSTAL_CS4238B:
                        if (dev->pnp_size) {
                            dev->eeprom_data[26] = 0x38;
                            dev->eeprom_data[44] = '8';
                        }
                        dev->nvr_path = "cs4238b.nvr";
                        break;

                    case CRYSTAL_CS4235:
                        if (dev->pnp_size) {
                            dev->eeprom_data[26] = 0x25;
                            dev->eeprom_data[44] = '5';
                            dev->eeprom_data[45] = ' ';
                        }
                        dev->nvr_path = "cs4235.nvr";
                        break;

                    case CRYSTAL_CS4239:
                        if (dev->pnp_size) {
                            dev->eeprom_data[26] = 0x29;
                            dev->eeprom_data[44] = '9';
                            dev->eeprom_data[45] = ' ';
                        }
                        dev->nvr_path = "cs4239.nvr";
                        break;

                    default:
                        break;
                }

                /* Load EEPROM contents from file if present. */
                cs423x_nvram(dev, 0);
            }

            /* Initialize game port. The game port on all B chips only
               responds to 6 I/O ports; the remaining 2 are reserved. */
            dev->gameport = gameport_add((dev->ad1848_type == CRYSTAL_CS4236B) ? &gameport_pnp_6io_device : &gameport_pnp_device);

            break;

        default:
            break;
    }

    /* Initialize I2C bus for the EEPROM. */
    dev->i2c = i2c_gpio_init("nvr_cs423x");

    /* Initialize I2C EEPROM if enabled. */
    if (!(info->local & CRYSTAL_NOEEPROM))
        dev->eeprom = i2c_eeprom_init(i2c_gpio_get_bus(dev->i2c), 0x50, dev->eeprom_data, sizeof(dev->eeprom_data), 1);

    /* Initialize ISAPnP. */
    dev->pnp_card = isapnp_add_card(NULL, 0, cs423x_pnp_config_changed, NULL, NULL, NULL, dev);

    /* Initialize SBPro codec. The WSS codec is initialized later by cs423x_reset */
    dev->sb = device_add_inst(&sb_pro_compat_device, 1);
    sound_set_cd_audio_filter(sbpro_filter_cd_audio, dev->sb); /* CD audio filter for the default context */

    /* Initialize RAM, registers and WSS codec. */
    cs423x_reset(dev);
    sound_add_handler(cs423x_get_buffer, dev);
    music_add_handler(cs423x_get_music_buffer, dev);

    /* Add Control/RAM backdoor handlers for CS4235. */
    dev->ad1848.cram_priv  = dev;
    dev->ad1848.cram_read  = cs423x_read;
    dev->ad1848.cram_write = cs423x_write;

    return dev;
}

static void
cs423x_close(void *priv)
{
    cs423x_t *dev = (cs423x_t *) priv;

    cs423x_log("CS423x: close()\n");

    /* Save EEPROM contents to file. */
    if (dev->eeprom) {
        cs423x_nvram(dev, 1);
        i2c_eeprom_close(dev->eeprom);
    }

    i2c_gpio_close(dev->i2c);

    free(dev);
}

static int
cs423x_available(void)
{
    return rom_present(PNP_ROM_CS4236B);
}

static void
cs423x_speed_changed(void *priv)
{
    cs423x_t *dev = (cs423x_t *) priv;

    ad1848_speed_changed(&dev->ad1848);
}

const device_t cs4235_device = {
    .name          = "Crystal CS4235",
    .internal_name = "cs4235",
    .flags         = DEVICE_ISA16,
    .local         = CRYSTAL_CS4235,
    .init          = cs423x_init,
    .close         = cs423x_close,
    .reset         = cs423x_reset,
    .available     = cs423x_available,
    .speed_changed = cs423x_speed_changed,
    .force_redraw  = NULL,
    .config        = NULL
};

const device_t cs4235_onboard_device = {
    .name          = "Crystal CS4235 (On-Board)",
    .internal_name = "cs4235_onboard",
    .flags         = DEVICE_ISA16,
    .local         = CRYSTAL_CS4235 | CRYSTAL_NOEEPROM,
    .init          = cs423x_init,
    .close         = cs423x_close,
    .reset         = cs423x_reset,
    .available     = cs423x_available,
    .speed_changed = cs423x_speed_changed,
    .force_redraw  = NULL,
    .config        = NULL
};

const device_t cs4236_onboard_device = {
    .name          = "Crystal CS4236 (On-Board)",
    .internal_name = "cs4236_onboard",
    .flags         = DEVICE_ISA16,
    .local         = CRYSTAL_CS4236 | CRYSTAL_NOEEPROM,
    .init          = cs423x_init,
    .close         = cs423x_close,
    .reset         = cs423x_reset,
    .available     = cs423x_available,
    .speed_changed = cs423x_speed_changed,
    .force_redraw  = NULL,
    .config        = NULL
};

const device_t cs4236b_device = {
    .name          = "Crystal CS4236B",
    .internal_name = "cs4236b",
    .flags         = DEVICE_ISA16,
    .local         = CRYSTAL_CS4236B,
    .init          = cs423x_init,
    .close         = cs423x_close,
    .reset         = cs423x_reset,
    .available     = cs423x_available,
    .speed_changed = cs423x_speed_changed,
    .force_redraw  = NULL,
    .config        = NULL
};

const device_t cs4236b_onboard_device = {
    .name          = "Crystal CS4236B",
    .internal_name = "cs4236b",
    .flags         = DEVICE_ISA16,
    .local         = CRYSTAL_CS4236B | CRYSTAL_NOEEPROM,
    .init          = cs423x_init,
    .close         = cs423x_close,
    .reset         = cs423x_reset,
    .available     = cs423x_available,
    .speed_changed = cs423x_speed_changed,
    .force_redraw  = NULL,
    .config        = NULL
};

const device_t cs4237b_device = {
    .name          = "Crystal CS4237B",
    .internal_name = "cs4237b",
    .flags         = DEVICE_ISA16,
    .local         = CRYSTAL_CS4237B,
    .init          = cs423x_init,
    .close         = cs423x_close,
    .reset         = cs423x_reset,
    .available     = cs423x_available,
    .speed_changed = cs423x_speed_changed,
    .force_redraw  = NULL,
    .config        = NULL
};

const device_t cs4238b_device = {
    .name          = "Crystal CS4238B",
    .internal_name = "cs4238b",
    .flags         = DEVICE_ISA16,
    .local         = CRYSTAL_CS4238B,
    .init          = cs423x_init,
    .close         = cs423x_close,
    .reset         = cs423x_reset,
    .available     = cs423x_available,
    .speed_changed = cs423x_speed_changed,
    .force_redraw  = NULL,
    .config        = NULL
};
