/*
 * 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-2022 RichardG.
 */
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.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_CS4235  = 0xdd,
    CRYSTAL_CS4236B = 0xcb,
    CRYSTAL_CS4237B = 0xc8,
    CRYSTAL_CS4238B = 0xc9
};
enum {
    CRYSTAL_SLAM_NONE  = 0,
    CRYSTAL_SLAM_INDEX = 1,
    CRYSTAL_SLAM_BYTE1 = 2,
    CRYSTAL_SLAM_BYTE2 = 3
};

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 cs4236b_eeprom[8224]  = {
    // clang-format off
    /* Chip configuration */
    0x55, 0xbb, /* magic */
    0x00, 0x00, /* length */
    0x00, 0x03, /* CD-ROM and modem decode */
    0x80, /* misc. config */
    0x80, /* global config */
    0x0b, /* chip ID */
    0x20, 0x04, 0x08, 0x10, 0x80, 0x00, 0x00, /* reserved */
    0x00, /* external decode length */
    0x48, /* reserved */
    0x75, 0xb9, 0xfc, /* IRQ routing */
    0x10, 0x03, /* DMA routing */

    /* PnP resources */
    0x00
    // 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;
    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_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_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);
    }
}

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 4: /* Control Indirect Data Register */
            ret = dev->indirect_regs[dev->regs[3]];
            break;

        case 5: /* Control/RAM Access */
            /* Reading RAM is undocumented; the Windows drivers do so. */
            if (dev->ram_dl == 3)
                ret = dev->ram_data[dev->ram_addr++];
            break;

        case 7: /* Global Status */
            /* 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;
    }

    return ret;
}

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

    switch (reg) {
        case 1: /* EEPROM Interface */
            if (val & 0x04)
                i2c_gpio_set(dev->i2c, val & 0x01, val & 0x02);
            break;

        case 3: /* Control Indirect Access Register */
            val &= 0x0f;
            break;

        case 4: /* Control Indirect Data Register */
            switch (dev->regs[3] & 0x0f) {
                case 0: /* WSS Master Control */
                    if (val & 0x80)
                        ad1848_init(&dev->ad1848, dev->ad1848_type);
                    val = 0x00;
                    break;

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

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

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

                case 4: /* Consumer Serial Port Enable */
                    if (dev->type < CRYSTAL_CS4237B)
                        return;
                    val &= 0xf0;
                    break;

                case 5: /* Lower Channel Status */
                    if (dev->type < CRYSTAL_CS4237B)
                        return;
                    val &= 0xfe;
                    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;

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

        case 5: /* Control/RAM Access */
            switch (dev->ram_dl) {
                case 0: /* 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 = 1;
                            break;

                        default:
                            break;
                    }
                    break;

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

                case 2: /* high address byte */
                    dev->ram_addr |= (val << 8);
                    dev->ram_dl++;
                    break;

                case 3: /* data */
                    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)) {
                dev->ram_dl = 0;

                /* Update PnP state and resource data. */
                cs423x_pnp_enable(dev, 1, 0);
            }
            break;

        case 7: /* Global Status */
            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;

    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. */
                    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) {
                /* 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. */
            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 *) malloc(sizeof(isapnp_device_config_t));

                    /* Start new logical device. */
                    memset(dev->slam_config, 0, sizeof(isapnp_device_config_t));
                    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 read 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)) {
        dev->slam_enable = 1;
        io_sethandler(0x279, 1, NULL, NULL, NULL, cs423x_slam_write, NULL, NULL, dev);
    }
}

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;

        /* 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 is powered down. */
    if (!(dev->indirect_regs[2] & 0xa4)) {
        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;
    int             opl_wss = dev->opl_wss;
    const int32_t  *opl_buf = NULL;

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

    /* Don't output anything if the analog section is powered down. */
    if (!(dev->indirect_regs[2] & 0xa4)) {
        for (int c = 0; c < len * 2; c += 2) {
            if (opl_wss) {
                buffer[c] += (opl_buf[c] * dev->ad1848.fm_vol_l) >> 16;
                buffer[c + 1] += (opl_buf[c + 1] * dev->ad1848.fm_vol_r) >> 16;
            }
        }
    }

    if (opl_wss)
        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)
{
    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], 384);

        /* Disable PnP key if the PKD bit is set, or if it was disabled by command 0x55. */
        /* But wait! The TriGem Delhi-III BIOS sends command 0x55, and its behavior doesn't
           line up with real hardware (still listed in the POST summary and seen by software).
           Disable the PnP key disabling mechanism until someone figures something out. */
#if 0
        isapnp_enable_card(dev->pnp_card, ((dev->ram_data[0x4002] & 0x20) || !dev->pnp_enable) ? ISAPNP_CARD_NO_KEY : ISAPNP_CARD_ENABLE);
#endif
        if ((dev->ram_data[0x4002] & 0x20) || !dev->pnp_enable)
            pclog("CS423x: Attempted to disable PnP key\n");
    }

    /* 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_CS4235) {
            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;

        /* 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_reset(void *priv)
{
    cs423x_t *dev = (cs423x_t *) priv;

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

    if (dev->eeprom) {
        /* Load EEPROM data to RAM. */
        memcpy(&dev->ram_data[0x4000], &dev->eeprom_data[4], MIN(384, ((dev->eeprom_data[2] << 8) | dev->eeprom_data[3]) - 4));

        /* 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);

    /* 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 = malloc(sizeof(cs423x_t));
    memset(dev, 0, sizeof(cs423x_t));

    /* Initialize model-specific data. */
    dev->type = info->local & 0xff;
    switch (dev->type) {
        case CRYSTAL_CS4235:
        case CRYSTAL_CS4236B:
        case CRYSTAL_CS4237B:
        case CRYSTAL_CS4238B:
            /* Same WSS codec and EEPROM structure. */
            dev->ad1848_type = (dev->type == CRYSTAL_CS4235) ? AD1848_TYPE_CS4235 : AD1848_TYPE_CS4236;
            dev->pnp_offset  = 0x4013;

            /* Different Chip Version and ID registers, which shouldn't be reset by ad1848_init */
            dev->ad1848.xregs[25] = dev->type;

            if (!(info->local & CRYSTAL_NOEEPROM)) {
                /* Load EEPROM contents from template. */
                memcpy(dev->eeprom_data, cs4236b_eeprom, sizeof(cs4236b_eeprom));

                FILE *fp = rom_fopen(PNP_ROM_CS4236B, "rb");
                if (fp) {
                    (void) !fread(&(dev->eeprom_data[23]), 1, 8201, fp);
                    fclose(fp);
                }

                /* Set content size. */
                dev->eeprom_data[2] = sizeof(cs4236b_eeprom) >> 8;
                dev->eeprom_data[3] = sizeof(cs4236b_eeprom) & 0xff;

                /* Set PnP card ID and EEPROM file name. */
                switch (dev->type) {
                    case CRYSTAL_CS4235:
                        dev->eeprom_data[8]  = 0x05;
                        dev->eeprom_data[16] = 0x08;
                        dev->eeprom_data[26] = 0x25;
                        dev->eeprom_data[44] = '5';
                        dev->nvr_path        = "cs4235.nvr";
                        break;

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

                    case CRYSTAL_CS4237B:
                        dev->eeprom_data[26] = 0x37;
                        dev->eeprom_data[44] = '7';
                        dev->nvr_path        = "cs4237b.nvr";
                        break;

                    case CRYSTAL_CS4238B:
                        dev->eeprom_data[26] = 0x38;
                        dev->eeprom_data[44] = '8';
                        dev->nvr_path        = "cs4238b.nvr";
                        break;

                    default:
                        break;
                }

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

            /* Initialize game port. The '7B and '8B game port only responds to 6 I/O ports; the remaining
               2 ports are reserved on those chips, and probably connected to the Digital Assist feature. */
            dev->gameport = gameport_add(((dev->type == CRYSTAL_CS4235) || (dev->type == CRYSTAL_CS4236B)) ? &gameport_pnp_device : &gameport_pnp_6io_device);

            break;

        default:
            break;
    }

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

    /* Initialize I2C EEPROM if the contents are valid. */
    if ((dev->eeprom_data[0] == 0x55) && (dev->eeprom_data[1] == 0xbb))
        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;

    /* 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_ISA | DEVICE_AT,
    .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_ISA | DEVICE_AT,
    .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 cs4236b_device = {
    .name          = "Crystal CS4236B",
    .internal_name = "cs4236b",
    .flags         = DEVICE_ISA | DEVICE_AT,
    .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 cs4237b_device = {
    .name          = "Crystal CS4237B",
    .internal_name = "cs4237b",
    .flags         = DEVICE_ISA | DEVICE_AT,
    .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_ISA | DEVICE_AT,
    .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
};
