#include <stdint.h>
#include <stdlib.h>
#include "ibm.h"
#include "device.h"
#include "io.h"
#include "sound.h"
#include "sound_pssj.h"
#include "sound_sn76489.h"

#include "dma.h"
#include "pic.h"
#include "timer.h"

typedef struct pssj_t
{
        sn76489_t sn76489;
        
        uint8_t ctrl;
        uint8_t wave;
        uint8_t dac_val;
        uint16_t freq;
        int amplitude;
        
        int irq;
        int64_t timer_count;
        int enable;
        
        int wave_pos;
        int pulse_width;

        int16_t buffer[SOUNDBUFLEN];
        int pos;
} pssj_t;

static void pssj_update_irq(pssj_t *pssj)
{
        if (pssj->irq && (pssj->ctrl & 0x10) && (pssj->ctrl & 0x08))
                picint(1 << 7);
}

static void pssj_write(uint16_t port, uint8_t val, void *p)
{
        pssj_t *pssj = (pssj_t *)p;
        
//        pclog("pssj_write: port=%04x val=%02x\n", port, val);
        switch (port & 3)
        {
                case 0:
                pssj->ctrl = val;
                pssj->enable = (val & 4) && (pssj->ctrl & 3);
                sn74689_set_extra_divide(&pssj->sn76489, val & 0x40);
                if (!(val & 8))
                        pssj->irq = 0;
                pssj_update_irq(pssj);
                break;
                case 1:
                switch (pssj->ctrl & 3)
                {
                        case 1: /*Sound channel*/
                        pssj->wave = val;
                        pssj->pulse_width = val & 7;
                        break;
                        case 3: /*Direct DAC*/
                        pssj->dac_val = val;
                        break;
                }
                break;
                case 2:
                pssj->freq = (pssj->freq & 0xf00) | val;
                break;
                case 3:
                pssj->freq = (pssj->freq & 0x0ff) | ((val & 0xf) << 8);
                pssj->amplitude = val >> 4;
                break;
        }       
}
static uint8_t pssj_read(uint16_t port, void *p)
{
        pssj_t *pssj = (pssj_t *)p;
        
//        pclog("pssj_read: port=%04x %02x\n", port, (pssj->ctrl & ~0x88) | (pssj->irq ? 8 : 0));
        switch (port & 3)
        {
                case 0:
                return (pssj->ctrl & ~0x88) | (pssj->irq ? 8 : 0);
                case 1:
                switch (pssj->ctrl & 3)
                {
                        case 0: /*Joystick*/
                        return 0;
                        case 1: /*Sound channel*/
                        return pssj->wave;
                        case 2: /*Successive approximation*/
                        return 0x80;
                        case 3: /*Direct DAC*/
                        return pssj->dac_val;
                }
                break;
                case 2:
                return pssj->freq & 0xff;
                case 3:
                return (pssj->freq >> 8) | (pssj->amplitude << 4);
        }
}

static void pssj_update(pssj_t *pssj)
{
        for (; pssj->pos < sound_pos_global; pssj->pos++)        
                pssj->buffer[pssj->pos] = (((int8_t)(pssj->dac_val ^ 0x80) * 0x20) * pssj->amplitude) / 15;
}

static void pssj_callback(void *p)
{
        pssj_t *pssj = (pssj_t *)p;
        int data;
        
        pssj_update(pssj);
        if (pssj->ctrl & 2)
        {
                if ((pssj->ctrl & 3) == 3)
                {
                        data = dma_channel_read(1);

                        if (data != DMA_NODATA)
                        {
                                pssj->dac_val = data & 0xff;
//                                pclog("DAC_val=%02x\n", data);
                        }
                }
                else
                {
                        data = dma_channel_write(1, 0x80);
                }

                if ((data & DMA_OVER) && data != DMA_NODATA)
                {
//                        pclog("Check IRQ %i %02x\n", pssj->irq, pssj->ctrl);
                        if (pssj->ctrl & 0x08)
                        {
                                pssj->irq = 1;
                                pssj_update_irq(pssj);
                        }
                } 
        }
        else
        {
                switch (pssj->wave & 0xc0)
                {
                        case 0x00: /*Pulse*/
                        pssj->dac_val = (pssj->wave_pos > (pssj->pulse_width << 1)) ? 0xff : 0;
                        break;
                        case 0x40: /*Ramp*/
                        pssj->dac_val = pssj->wave_pos << 3;
                        break;
                        case 0x80: /*Triangle*/
                        if (pssj->wave_pos & 16)
                                pssj->dac_val = (pssj->wave_pos ^ 31) << 4;
                        else
                                pssj->dac_val = pssj->wave_pos << 4;
                        break;
                        case 0xc0:
                        pssj->dac_val = 0x80;
                        break;
                }
                pssj->wave_pos = (pssj->wave_pos + 1) & 31;
        }

        pssj->timer_count += (int)(TIMER_USEC * (1000000.0 / 3579545.0) * (double)(pssj->freq ? pssj->freq : 0x400));
}

static void pssj_get_buffer(int32_t *buffer, int len, void *p)
{
        pssj_t *pssj = (pssj_t *)p;
        int c;
        
        pssj_update(pssj);
        
        for (c = 0; c < len * 2; c++)
                buffer[c] += pssj->buffer[c >> 1];

        pssj->pos = 0;
}

void *pssj_init()
{
        pssj_t *pssj = malloc(sizeof(pssj_t));
        memset(pssj, 0, sizeof(pssj_t));

        sn76489_init(&pssj->sn76489, 0x00c0, 0x0004, PSSJ, 3579545);

        io_sethandler(0x00C4, 0x0004, pssj_read, NULL, NULL, pssj_write, NULL, NULL, pssj);
        timer_add(pssj_callback, &pssj->timer_count, &pssj->enable, pssj);
        sound_add_handler(pssj_get_buffer, pssj);
        
        return pssj;
}

void pssj_close(void *p)
{
        pssj_t *pssj = (pssj_t *)p;

        free(pssj);        
}

device_t pssj_device =
{
        "Tandy PSSJ",
        0,
        pssj_init,
        pssj_close,
        NULL,
        NULL,
        NULL,
        NULL
};
