/*Mitsumi CD-ROM drive emulation
  Currently emulates an FX001D (double speed drive supplied with HCCS Ultimate CD-ROM interface)
*/
#include <stdint.h>
#include "mitsumi.h"
#include "ultimatecdrom.h"
#include "cdrom.h"

ATAPI *atapi;

/*Constants mainly borrowed from Linux & FreeBSD MCD drivers*/
#define STATUS_CMD_CHECK   (1 << 0)
#define STATUS_BUSY        (1 << 1)
#define STATUS_READ_ERROR  (1 << 2)
#define STATUS_DISC_TYPE   (1 << 3)
#define STATUS_SERVO_CHECK (1 << 4)
#define STATUS_DISC_CHANGE (1 << 5)
#define STATUS_DISC_READY  (1 << 6)
#define STATUS_DOOR_OPEN   (1 << 7)

#define FLAGS_DATA_AVAIL   (1 << 1)
#define FLAGS_STATUS_AVAIL (1 << 2)

#define MODE_PLAY_AUDIO    (1 << 0)
#define MODE_TOC           (1 << 2)
#define MODE_DATA_LENGTH   (1 << 6)

#define CMD_GET_VOL_INFO   0x10
#define CMD_GET_DISC_INFO  0x11
#define CMD_GET_Q_CHANNEL  0x20
#define CMD_GET_STATUS     0x40
#define CMD_SET_MODE       0x50
#define CMD_SOFT_RESET     0x60
#define CMD_STOP_AUDIO     0x70
#define CMD_CONFIG_DRIVE   0x90
#define CMD_SET_DRIVE_MODE 0xa0
#define CMD_SET_VOLUME     0xae
#define CMD_PLAY_READ      0xc0
#define CMD_PLAY_READ_2X   0xc1
#define CMD_GET_DRIVE_MODE 0xc2
#define CMD_GET_VERSION    0xdc
#define CMD_STOP           0xf0
#define CMD_EJECT          0xf6

static void mitsumi_reset_params(mitsumi_t *mitsumi)
{
        mitsumi->params_wp = 0;
        mitsumi->nr_params = 0;
}

static void mitsumi_reset_status(mitsumi_t *mitsumi)
{
        mitsumi->status_rp = 0;
        mitsumi->status_wp = 0;
}

static void mitsumi_add_status(mitsumi_t *mitsumi, uint8_t val)
{
        mitsumi->status_dat[mitsumi->status_wp++] = val;
}

static void mitsumi_command_start(mitsumi_t *mitsumi, uint8_t cmd)
{
        mitsumi->cmd = cmd;
        
        switch (cmd)
        {
                case CMD_GET_VERSION:
                mitsumi->state = CMD_STATE_BUSY;
                mitsumi->cmd_time = 2;
                break;
                
                case CMD_GET_STATUS:
                mitsumi->state = CMD_STATE_BUSY;
                mitsumi->cmd_time = 2;
                break;

                case CMD_SOFT_RESET:
                mitsumi->state = CMD_STATE_BUSY;
                mitsumi->cmd_time = 2;
                break;
                
                case CMD_GET_DISC_INFO:
                mitsumi->state = CMD_STATE_BUSY;
                mitsumi->cmd_time = 2;
                break;

                case CMD_GET_DRIVE_MODE:
                mitsumi->state = CMD_STATE_BUSY;
                mitsumi->cmd_time = 2;
                break;

                case CMD_GET_VOL_INFO:
                mitsumi->state = CMD_STATE_BUSY;
                mitsumi->cmd_time = 2;
                break;

                case CMD_GET_Q_CHANNEL:
                mitsumi->state = CMD_STATE_BUSY;
                mitsumi->cmd_time = 2;
                break;

                case CMD_STOP:
                mitsumi->state = CMD_STATE_BUSY;
                mitsumi->cmd_time = 2;
                break;

                case CMD_STOP_AUDIO:
                mitsumi->state = CMD_STATE_BUSY;
                mitsumi->cmd_time = 2;
                break;

                case CMD_EJECT:
                mitsumi->state = CMD_STATE_BUSY;
                mitsumi->cmd_time = 2;
                break;

                case CMD_SET_MODE:
                mitsumi_reset_params(mitsumi);
                mitsumi->nr_params = 1;
                mitsumi->state = CMD_STATE_PARAMS;
                break;

                case CMD_SET_DRIVE_MODE:
                mitsumi_reset_params(mitsumi);
                mitsumi->nr_params = 1;
                mitsumi->state = CMD_STATE_PARAMS;
                break;

                case CMD_CONFIG_DRIVE:
                mitsumi_reset_params(mitsumi);
                mitsumi->nr_params = 2;
                mitsumi->state = CMD_STATE_PARAMS;
                break;

                case CMD_PLAY_READ:
                mitsumi_reset_params(mitsumi);
                mitsumi->nr_params = 6;
                mitsumi->state = CMD_STATE_PARAMS;
                mitsumi->sector_count = 0;
                break;

                case CMD_PLAY_READ_2X:
                mitsumi_reset_params(mitsumi);
                mitsumi->nr_params = 6;
                mitsumi->state = CMD_STATE_PARAMS;
                mitsumi->sector_count = 0;
                break;

                default:
                cdfatal("mitsumi_start_command: Unknown MCD command %02x\n", cmd);
                break;
        }
        cdlog("mitsumi_command_start: cmd=%02x cmd_time=%i\n", cmd, mitsumi->cmd_time);
}

static int msf_to_sector(int m, int s, int f)
{
        /*Convert BCD -> bin*/
        m = (m & 0xf) + ((m >> 4) * 10);
        s = (s & 0xf) + ((s >> 4) * 10);
        f = (f & 0xf) + ((f >> 4) * 10);
                
        return ((((m * 60) + s) * 75) + f) - 150;
}

static uint8_t bin_to_bcd(uint8_t in)
{
        uint8_t out = in % 10;
        out += ((in / 10) << 4);
        
        return out;
}

static void mitsumi_add_status_disc_changed(mitsumi_t *mitsumi)
{
        mitsumi_add_status(mitsumi, STATUS_CMD_CHECK | STATUS_DISC_CHANGE | STATUS_DOOR_OPEN);
        mitsumi->state = CMD_STATE_STATUS;
        cdlog("Disc changed\n");
}

static void mitsumi_command_run(mitsumi_t *mitsumi)
{
        cdlog("mitsumi_command_run %02x\n", mitsumi->cmd);
        switch (mitsumi->cmd)
        {
                case CMD_GET_VERSION:
                mitsumi_reset_status(mitsumi);
                mitsumi_add_status(mitsumi, 0);
                mitsumi_add_status(mitsumi, 'D');
                mitsumi_add_status(mitsumi, 3);
                mitsumi->state = CMD_STATE_STATUS;
                break;

                case CMD_GET_STATUS:
                mitsumi_reset_status(mitsumi);
                if (atapi->status() == CD_STATUS_EMPTY)
                        mitsumi_add_status(mitsumi, STATUS_DOOR_OPEN);
                else if (atapi->medium_changed())
                        mitsumi_add_status(mitsumi, STATUS_DISC_CHANGE | STATUS_DOOR_OPEN);
                else if (atapi->status() == CD_STATUS_PLAYING)
                        mitsumi_add_status(mitsumi, STATUS_BUSY | STATUS_DISC_READY);
                else
                        mitsumi_add_status(mitsumi, mitsumi->status);
                mitsumi->state = CMD_STATE_STATUS;
                break;

                case CMD_SOFT_RESET:
                mitsumi_reset_status(mitsumi);
                mitsumi_add_status(mitsumi, 0);
                mitsumi->state = CMD_STATE_STATUS;
                break;

                case CMD_GET_DISC_INFO:
                mitsumi_reset_status(mitsumi);
                if (atapi->medium_changed())
                        mitsumi_add_status_disc_changed(mitsumi);
                else
                {
                        mitsumi_add_status(mitsumi, 0);
                        mitsumi_add_status(mitsumi, 0);
                        mitsumi_add_status(mitsumi, 0);
                        mitsumi_add_status(mitsumi, 0);
                        mitsumi_add_status(mitsumi, 0);
                        mitsumi->state = CMD_STATE_STATUS;
                }
                break;

                case CMD_GET_VOL_INFO:
                mitsumi_reset_status(mitsumi);
                if (atapi->medium_changed())
                        mitsumi_add_status_disc_changed(mitsumi);
                else
                {
                        /*CD-ROM interface returns TOC suitable for ATAPI. Mangle a bit for MCD*/
                        uint8_t buf[12];
                        uint32_t size;
                        
                        /*Read TOC of first track*/
                        atapi->readtoc(buf, 1, 1, 12, 1);
                        size = atapi->size();
                        cdlog("ATAPI size = %i\n", size);

                        mitsumi_add_status(mitsumi, 0);
                
                        mitsumi_add_status(mitsumi, buf[2]); /*First track nr*/
                        mitsumi_add_status(mitsumi, buf[3]); /*Last track nr*/
                        mitsumi_add_status(mitsumi, bin_to_bcd(size / 4500)); /*Volume length*/
                        mitsumi_add_status(mitsumi, bin_to_bcd((size % 4500) / 75));
                        mitsumi_add_status(mitsumi, bin_to_bcd(size % 75));
                        mitsumi_add_status(mitsumi, bin_to_bcd(buf[9])); /*Track 1 start*/
                        mitsumi_add_status(mitsumi, bin_to_bcd(buf[10]));
                        mitsumi_add_status(mitsumi, bin_to_bcd(buf[11]));
                }
                mitsumi->state = CMD_STATE_STATUS;
                break;

                case CMD_GET_Q_CHANNEL:
                mitsumi_reset_status(mitsumi);

                if (atapi->medium_changed())
                        mitsumi_add_status_disc_changed(mitsumi);
                else if (mitsumi->mode & MODE_TOC)
                {
                        int toc_len;
                        int offset;
                        uint32_t size;
                        
                        atapi->medium_changed();
                        toc_len = atapi->readtoc_raw(mitsumi->toc, 2048);
                        size = atapi->size();
                        
                        offset = (mitsumi->read_toc_track*11) + 4;
                        if (offset >= toc_len)
                        {
                                cdlog("CMD_GET_Q_CHANNEL: end of CD! track %i\n", mitsumi->read_toc_track);
//                                mitsumi_add_status(mitsumi, 0x41);
                                mitsumi->read_toc_track = 0;
//                                offset = (mitsumi->read_toc_track*8) + 4;
                                mitsumi_add_status(mitsumi, 0);
                                
                                mitsumi_add_status(mitsumi, 0x54);
                                mitsumi_add_status(mitsumi, /*2*/0xd);
                                mitsumi_add_status(mitsumi, 0);
                                mitsumi_add_status(mitsumi, 0);
                                mitsumi_add_status(mitsumi, 0);
                                mitsumi_add_status(mitsumi, 0);
                                mitsumi_add_status(mitsumi, 0);
                                mitsumi_add_status(mitsumi, 0);
                                mitsumi_add_status(mitsumi, 0);
                                mitsumi_add_status(mitsumi, 0);
                        }
                        else
                        {
                                cdlog("TOC track %02x %02x %02x %02x %02x %02x %02x %02x   %i %i\n",
                                        mitsumi->toc[offset+0],mitsumi->toc[offset+1],mitsumi->toc[offset+2],mitsumi->toc[offset+3],
                                        mitsumi->toc[offset+4],mitsumi->toc[offset+5],mitsumi->toc[offset+6],mitsumi->toc[offset+7], offset, toc_len);
                                mitsumi_add_status(mitsumi, 0);
                                mitsumi_add_status(mitsumi, mitsumi->toc[offset+1]); /*Control + Adr*/
                                mitsumi_add_status(mitsumi, 0);
                                mitsumi_add_status(mitsumi, mitsumi->toc[offset+3]); /*Point index*/
                                mitsumi_add_status(mitsumi, bin_to_bcd(mitsumi->toc[offset+4])); /*Track M*/
                                mitsumi_add_status(mitsumi, bin_to_bcd(mitsumi->toc[offset+5])); /*Track S*/
                                mitsumi_add_status(mitsumi, bin_to_bcd(mitsumi->toc[offset+6])); /*Track F*/
                                mitsumi_add_status(mitsumi, 0);
                                mitsumi_add_status(mitsumi, bin_to_bcd(mitsumi->toc[offset+8]));
                                mitsumi_add_status(mitsumi, bin_to_bcd(mitsumi->toc[offset+9]));
                                mitsumi_add_status(mitsumi, bin_to_bcd(mitsumi->toc[offset+10]));

                                mitsumi->read_toc_track++;
                        }
                }
                else
                {
                        uint8_t subchannel_data[11];
                        
                        atapi->getcurrentsubchannel(subchannel_data, 1);
                        cdlog("CMD_GET_Q_CHANNEL: read Q channel\n");
                        if (atapi->status() == CD_STATUS_PLAYING)
                                mitsumi_add_status(mitsumi, STATUS_BUSY | STATUS_DISC_READY);
                        else
                                mitsumi_add_status(mitsumi, 0);
                        mitsumi_add_status(mitsumi, subchannel_data[0]); /*Control + ADR*/
                        mitsumi_add_status(mitsumi, subchannel_data[1]); /*Track*/
                        mitsumi_add_status(mitsumi, subchannel_data[2]); /*Index*/
                        mitsumi_add_status(mitsumi, bin_to_bcd(subchannel_data[8])); /*Track M*/
                        mitsumi_add_status(mitsumi, bin_to_bcd(subchannel_data[9])); /*Track S*/
                        mitsumi_add_status(mitsumi, bin_to_bcd(subchannel_data[10])); /*Track F*/
                        mitsumi_add_status(mitsumi, 0); /*Unused*/
                        mitsumi_add_status(mitsumi, bin_to_bcd(subchannel_data[4])); /*Disc M*/
                        mitsumi_add_status(mitsumi, bin_to_bcd(subchannel_data[5])); /*Disc M*/
                        mitsumi_add_status(mitsumi, bin_to_bcd(subchannel_data[6])); /*Disc M*/
                }

                mitsumi->state = CMD_STATE_STATUS;
                break;


                case CMD_SET_MODE:
                if ((mitsumi->params[0] & 4) && !(mitsumi->mode & 4))
                        mitsumi->read_toc_track = 0;
                mitsumi->mode = mitsumi->params[0];
                cdlog("CMD_SET_MODE: mode=%02x\n", mitsumi->mode);
                mitsumi_reset_status(mitsumi);
                mitsumi_add_status(mitsumi, 0);
                mitsumi->state = CMD_STATE_STATUS;
                break;

                case CMD_SET_DRIVE_MODE:
                mitsumi->drv_mode = mitsumi->params[0];
                cdlog("CMD_SET_DRIVE_MODE: mode=%02x\n", mitsumi->drv_mode);
                mitsumi_reset_status(mitsumi);
                mitsumi_add_status(mitsumi, 0);
                mitsumi->state = CMD_STATE_STATUS;
                break;

                case CMD_GET_DRIVE_MODE:
                mitsumi_reset_status(mitsumi);
                mitsumi_add_status(mitsumi, 0);
                mitsumi_add_status(mitsumi, mitsumi->drv_mode);
                mitsumi->state = CMD_STATE_STATUS;
                break;

                case CMD_CONFIG_DRIVE:
                cdlog("CMD_CONFIG_DRIVE: %02x %02x\n", mitsumi->params[0], mitsumi->params[1]);
                mitsumi_reset_status(mitsumi);
                mitsumi_add_status(mitsumi, 0);
                mitsumi->state = CMD_STATE_STATUS;
                break;

                case CMD_STOP:
                atapi->stop();
                mitsumi_reset_status(mitsumi);
                mitsumi_add_status(mitsumi, 0);
                mitsumi->state = CMD_STATE_STATUS;
                break;

                case CMD_STOP_AUDIO:
                atapi->stop();
                mitsumi_reset_status(mitsumi);
                mitsumi_add_status(mitsumi, 0);
                mitsumi->state = CMD_STATE_STATUS;
                break;

                case CMD_EJECT:
                atapi->eject();
                mitsumi_reset_status(mitsumi);
                mitsumi_add_status(mitsumi, 0);
                mitsumi->state = CMD_STATE_STATUS;
                break;

                case CMD_PLAY_READ:
                case CMD_PLAY_READ_2X:
                if (atapi->medium_changed())
                {
                        mitsumi_add_status_disc_changed(mitsumi);
                        mitsumi->state = CMD_STATE_STATUS;
                        break;
                }
                if (!mitsumi->sector_count)
                {
                        /*Load parameters*/
                        mitsumi->sector_pos = msf_to_sector(mitsumi->params[0], mitsumi->params[1], mitsumi->params[2]);
                        cdlog("nr_sectors=%i sector_pos=%i\n", mitsumi->nr_sectors_to_read, mitsumi->sector_pos);
                        if (atapi->is_track_audio(mitsumi->sector_pos, 0) && (mitsumi->mode & MODE_PLAY_AUDIO))
                        {
                                mitsumi->sector_end = msf_to_sector(mitsumi->params[3], mitsumi->params[4], mitsumi->params[5]);
                                cdlog("Playing audio track %08x - %08x\n", mitsumi->sector_pos, mitsumi->sector_end);
                                mitsumi->play_read = CMD_PLAY;
                        }
                        else
                        {
                                mitsumi->nr_sectors_to_read = mitsumi->params[5] |
                                                             (mitsumi->params[4] << 8) |
                                                             (mitsumi->params[3] << 16);
                                if (mitsumi->mode & MODE_DATA_LENGTH)
                                        mitsumi->play_read = CMD_READ_RAW;
                                else
                                        mitsumi->play_read = CMD_READ;
                        }
//                        if (mitsumi->nr_sectors_to_read > 20000)
//                                cdfatal("Here\n");
                }
                if (mitsumi->play_read == CMD_PLAY)
                {
                        if (mitsumi->sector_count)
                                cdfatal("Audio play should not get here...\n");
                        if (mitsumi->sector_pos >= mitsumi->sector_end)
                        {
                                cdlog("Play done\n");
                                mitsumi_reset_status(mitsumi);
                                mitsumi_add_status(mitsumi, 0);
                                mitsumi->state = CMD_STATE_STATUS;
                                break;
                        }
                        cdlog("Play audio!\n");
                        mitsumi->sector_count = 1;
                        atapi->playaudio(mitsumi->sector_pos, mitsumi->sector_end-mitsumi->sector_pos, 0);
                        mitsumi_reset_status(mitsumi);
                        mitsumi_add_status(mitsumi, STATUS_BUSY | STATUS_DISC_READY);
                        mitsumi->state = CMD_STATE_STATUS;
                }
                else if (mitsumi->play_read == CMD_READ)
                {
                        if (mitsumi->sector_count == mitsumi->nr_sectors_to_read)
                        {
                                cdlog("Read done\n");
                                mitsumi_reset_status(mitsumi);
                                mitsumi_add_status(mitsumi, 0);
                                mitsumi->state = CMD_STATE_STATUS;
                                break;
                        }
                        if (atapi->readsector(mitsumi->data_buf, mitsumi->sector_pos, 1))
                                cdfatal("Read failed\n");
                        mitsumi->sector_count++;
                        mitsumi->sector_pos++;
                        mitsumi->data_pos = 0;
                        mitsumi->data_len = 2048;
                        mitsumi->state = CMD_STATE_TRANSFER;
                }
                else
                {
                        if (mitsumi->sector_count == mitsumi->nr_sectors_to_read)
                        {
                                cdlog("Read done\n");
                                mitsumi_reset_status(mitsumi);
                                mitsumi_add_status(mitsumi, 0);
                                mitsumi->state = CMD_STATE_STATUS;
                                break;
                        }
                        atapi->readsector_raw(mitsumi->data_buf, mitsumi->sector_pos);
                        mitsumi->sector_count++;
                        mitsumi->sector_pos++;
                        mitsumi->data_pos = 0;
                        mitsumi->data_len = 2352;
                        mitsumi->state = CMD_STATE_TRANSFER;
                }
                break;

                default:
                cdfatal("mitsumi_command_run: Unknown MCD command %02x\n", mitsumi->cmd);
                break;
        }
}

void mitsumi_write_b(mitsumi_t *mitsumi, uint32_t addr, uint8_t val)
{
        switch (addr & 0xfc)
        {
                case 0x00: /*Command/data register*/
                cdlog("Write command reg: state=%i val=%02x\n", mitsumi->state, val);
                switch (mitsumi->state)
                {
                        case CMD_STATE_IDLE:
                        mitsumi_command_start(mitsumi, val);
                        break;
                        case CMD_STATE_PARAMS:
                        if (mitsumi->params_wp < mitsumi->nr_params)
                        {
                                mitsumi->params[mitsumi->params_wp++] = val;
                                if (mitsumi->params_wp == mitsumi->nr_params)
                                {
                                        mitsumi->state = CMD_STATE_BUSY;
                                        mitsumi->cmd_time = 2;
                                }
                        }
                        break;
                        case CMD_STATE_BUSY:
                        cdlog("mitsumi_write_b: CMD_STATE_BUSY\n");
                        break;
                        case CMD_STATE_STATUS:
                        cdlog("mitsumi_write_b: CMD_STATE_STATUS\n");
                        break;
                        case CMD_STATE_TRANSFER:
                        cdlog("mitsumi_write_b: CMD_STATE_TRANSFER\n");
                        break;
                }
                break;
                
                case 0x20: /*Flags register*/
                /*Read only?*/
                break;
        }
}

uint8_t mitsumi_read_b(mitsumi_t *mitsumi, uint32_t addr)
{
        uint8_t temp = 0xff;
        
        switch (addr & 0xfc)
        {
                case 0x00: /*Command/data register*/
                switch (mitsumi->state)
                {
                        case CMD_STATE_IDLE:
                        cdlog("mitsumi_read_b: CMD_STATE_IDLE\n");
                        temp = 0xff;
                        break;
                        case CMD_STATE_PARAMS:
                        cdlog("mitsumi_read_b: CMD_STATE_PARAMS\n");
                        temp = 0xff;
                        break;
                        case CMD_STATE_BUSY:
                        cdlog("mitsumi_read_b: CMD_STATE_BUSY\n");
                        temp = 0xff;
                        break;
                        case CMD_STATE_STATUS:
                        if (mitsumi->status_rp < mitsumi->status_wp)
                        {
                                temp = mitsumi->status_dat[mitsumi->status_rp++];
                                if (mitsumi->status_rp == mitsumi->status_wp)
                                        mitsumi->state = CMD_STATE_IDLE;
                        }
                        break;
                        case CMD_STATE_TRANSFER:
                        if (mitsumi->data_pos < mitsumi->data_len)
                        {
                                temp = mitsumi->data_buf[mitsumi->data_pos++];
                                if (mitsumi->data_pos == mitsumi->data_len)
                                {
                                        mitsumi->state = CMD_STATE_BUSY;
                                        mitsumi->cmd_time = 2;
                                }
                        }
//                        cdfatal("mitsumi_read_b: CMD_STATE_TRANSFER\n");
                        break;
                }
                break;
                
                case 0x20: /*Flags register*/
                switch (mitsumi->state)
                {
                        case CMD_STATE_IDLE:
                        temp = ~0;
                        break;
                        
                        case CMD_STATE_STATUS:
                        temp = ~FLAGS_STATUS_AVAIL;
                        break;
                        
                        case CMD_STATE_PARAMS:
                        temp = ~0;
                        break;

                        case CMD_STATE_BUSY:
                        temp = ~0;
                        break;

                        case CMD_STATE_TRANSFER:
                        temp = ~FLAGS_DATA_AVAIL;
                        break;
                }
                break;
        }
        
//        if ((addr != 0x3220 || temp != 0xff) && !(addr == 0x3200 && mitsumi->state == CMD_STATE_TRANSFER))
//                cdlog("mitsumi_read_b: addr=%04x temp=%02x\n", addr, temp);
        return temp;
}

void mitsumi_poll(mitsumi_t *mitsumi)
{
        if (mitsumi->cmd_time)
        {
                mitsumi->cmd_time--;
                if (!mitsumi->cmd_time)
                {
                        mitsumi_command_run(mitsumi);
                }
        }
}

void mitsumi_reset(mitsumi_t *mitsumi, const char *drive_path)
{
        mitsumi->status = STATUS_DISC_READY;
        
        ioctl_set_drive(drive_path);
        ioctl_reset();
        ioctl_open(0);
}
