#include <stdlib.h>
#include "ibm.h"
#include "device.h"
#include "io.h"
#include "pic.h"
#include "sound.h"
#include "sound_ps1.h"
#include "sound_sn76489.h"

typedef struct ps1_audio_t {
        sn76489_t sn76489;

        uint8_t status, ctrl;

        uint64_t timer_latch;
        pc_timer_t timer;

        uint8_t fifo[2048];
        int fifo_read_idx, fifo_write_idx;
        int fifo_threshold;

        uint8_t dac_val;

        int16_t buffer[MAXSOUNDBUFLEN];
        int pos;
} ps1_audio_t;

static void ps1_update_irq_status(ps1_audio_t *ps1) {
        if (((ps1->status & ps1->ctrl) & 0x12) && (ps1->ctrl & 0x01))
                picint(1 << 7);
        else
                picintc(1 << 7);
}

static uint8_t ps1_audio_read(uint16_t port, void *p) {
        ps1_audio_t *ps1 = (ps1_audio_t *)p;
        uint8_t temp;

        //        pclog("ps1_audio_read %04x %04x:%04x\n", port, CS, pc);

        switch (port & 7) {
        case 0: /*ADC data*/
                ps1->status &= ~0x10;
                ps1_update_irq_status(ps1);
                return 0;
        case 2: /*Status*/
                temp = ps1->status;
                temp |= (ps1->ctrl & 0x01);
                if ((ps1->fifo_write_idx - ps1->fifo_read_idx) >= 2048)
                        temp |= 0x08; /*FIFO full*/
                if (ps1->fifo_read_idx == ps1->fifo_write_idx)
                        temp |= 0x04; /*FIFO empty*/
                                      //                pclog("Return status %02x\n", temp);
                return temp;
        case 3: /*FIFO timer*/
                /*PS/1 technical reference says this should return the current value,
                  but the PS/1 BIOS and Stunt Island expect it not to change*/
                return ps1->timer_latch;
        case 4:
        case 5:
        case 6:
        case 7:
                return 0;
        }
        return 0xff;
}

static void ps1_audio_write(uint16_t port, uint8_t val, void *p) {
        ps1_audio_t *ps1 = (ps1_audio_t *)p;

        //        pclog("ps1_audio_write %04x %02x\n", port, val);

        switch (port & 7) {
        case 0: /*DAC output*/
                //                pclog("DAC write %08x %08x %i\n", ps1->fifo_write_idx, ps1->fifo_read_idx, ps1->fifo_write_idx -
                //                ps1->fifo_read_idx);
                if ((ps1->fifo_write_idx - ps1->fifo_read_idx) < 2048) {
                        ps1->fifo[ps1->fifo_write_idx & 2047] = val;
                        ps1->fifo_write_idx++;
                }
                break;
        case 2: /*Control*/
                ps1->ctrl = val;
                if (!(val & 0x02))
                        ps1->status &= ~0x02;
                ps1_update_irq_status(ps1);
                break;
        case 3: /*Timer reload value*/
                ps1->timer_latch = val;
                if (val)
                        timer_set_delay_u64(&ps1->timer, (0xff - val) * TIMER_USEC);
                else
                        timer_disable(&ps1->timer);
                break;
        case 4: /*Almost empty*/
                ps1->fifo_threshold = val * 4;
                break;
        }
}

static void ps1_audio_update(ps1_audio_t *ps1) {
        for (; ps1->pos < sound_pos_global; ps1->pos++)
                ps1->buffer[ps1->pos] = (int8_t)(ps1->dac_val ^ 0x80) * 0x20;
}

static void ps1_audio_callback(void *p) {
        ps1_audio_t *ps1 = (ps1_audio_t *)p;

        ps1_audio_update(ps1);

        if (ps1->fifo_read_idx != ps1->fifo_write_idx) {
                ps1->dac_val = ps1->fifo[ps1->fifo_read_idx & 2047];
                ps1->fifo_read_idx++;
        }
        //        pclog("ps1_callback %08x %08x %08x\n", ps1->fifo_write_idx, ps1->fifo_read_idx, ps1->fifo_threshold);
        if ((ps1->fifo_write_idx - ps1->fifo_read_idx) == ps1->fifo_threshold) {
                //                pclog("FIFO almost empty\n");
                ps1->status |= 0x02; /*FIFO almost empty*/
        }
        ps1->status |= 0x10; /*ADC data ready*/
        ps1_update_irq_status(ps1);

        timer_advance_u64(&ps1->timer, ps1->timer_latch * TIMER_USEC);
}

static void ps1_audio_get_buffer(int32_t *buffer, int len, void *p) {
        ps1_audio_t *ps1 = (ps1_audio_t *)p;
        int c;

        ps1_audio_update(ps1);

        for (c = 0; c < len * 2; c++)
                buffer[c] += ps1->buffer[c >> 1];

        ps1->pos = 0;
}

static void *ps1_audio_init() {
        ps1_audio_t *ps1 = malloc(sizeof(ps1_audio_t));
        memset(ps1, 0, sizeof(ps1_audio_t));

        sn76489_init(&ps1->sn76489, 0x0205, 0x0001, SN76496, 4000000);

        io_sethandler(0x0200, 0x0001, ps1_audio_read, NULL, NULL, ps1_audio_write, NULL, NULL, ps1);
        io_sethandler(0x0202, 0x0006, ps1_audio_read, NULL, NULL, ps1_audio_write, NULL, NULL, ps1);
        timer_add(&ps1->timer, ps1_audio_callback, ps1, 0);
        sound_add_handler(ps1_audio_get_buffer, ps1);

        return ps1;
}

static void ps1_audio_close(void *p) {
        ps1_audio_t *ps1 = (ps1_audio_t *)p;

        free(ps1);
}

device_t ps1_audio_device = {"PS/1 Audio Card", 0, ps1_audio_init, ps1_audio_close, NULL, NULL, NULL, NULL};
