/*
 * Sega Saturn USB flash cart ROM
 * by Anders Montonen, 2012
 *
 * Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0)
 *
 */

#include <stdint.h>

#include "vdp2.h"
#include "scu.h"

#include "crc.h"

#define USB_FLAGS (*(volatile uint8_t*)(0x22200001))
#define USB_RXF     (1 << 0)
#define USB_TXE     (1 << 1)
#define USB_PWREN   (1 << 7)
#define USB_FIFO (*(volatile uint8_t*)(0x22100001))

#define RGB(r, g, b) ((((b)&0x1f)<<10)|(((g)&0x1f)<<5)|((r)&0x1f))

enum
{
    USBDC_FUNC_DOWNLOAD      = 1,
    USBDC_FUNC_UPLOAD        = 2,
    USBDC_FUNC_EXEC          = 3,
    USBDC_FUNC_GET_BUFF_ADDR = 4,
    USBDC_FUNC_COPYEXEC      = 5,


    USBDC_FUNC_EXIT          = 0xFE,
};


const uint16_t ColorTable[] =
{
    RGB(   0,    0,    0), /* black  */
    RGB(0x1f,    0,    0), /* red    */
    RGB(   0, 0x0f,    0), /* green  */
    RGB(0x1f, 0x13,    0), /* orange */
    RGB(   0,    0, 0x0f), /* blue   */
};

typedef enum
{
    BLACK,
    RED,
    GREEN,
    ORANGE,
    BLUE,
} Color_e;

static void SetScreenColor(Color_e color)
{
    *(uint16_t*)VDP2_VRAM = ColorTable[color];
}

static void SignalError(void)
{
    /* Display a red screen for two seconds. */
    int delay = 60*2; /* Doesn't take 50Hz display into account.. */

    SetScreenColor(RED);
    while(delay > 0)
    {
        WaitForVBLANKIn();
        WaitForVBLANKOut();
        --delay;
    }
}


#define DDR1        (*(volatile unsigned char  *)0x20100079)
#define DDR2        (*(volatile unsigned char  *)0x2010007B)
#define IOSEL       (*(volatile unsigned char  *)0x2010007D)
#define EXLE        (*(volatile unsigned char  *)0x2010007F)
#define PDR1        (*(volatile unsigned char  *)0x20100075)
#define PDR2        (*(volatile unsigned char  *)0x20100077)

#define PAD_LBUTTON (1<<15)
#define PAD_RBUTTON (1<< 3)
#define PAD_X       (1<< 2)
#define PAD_Y       (1<< 1)
#define PAD_Z       (1<< 0)
#define PAD_START   (1<<11)
#define PAD_A       (1<<10)
#define PAD_C       (1<< 9)
#define PAD_B       (1<< 8)
#define PAD_RIGHT   (1<< 7)
#define PAD_LEFT    (1<< 6)
#define PAD_DOWN    (1<< 5)
#define PAD_UP      (1<< 4)

#define PAD_DELAY 16
static uint16_t GetPadStatus(int pad_connected)
{
    uint16_t temp;
    uint16_t count;

    if(pad_connected & 0x1)
    { /* Read input from pad1 */
        for(count=PAD_DELAY; count; count--)
        {
            PDR1 = 0x60;
        }
        temp = (PDR1 & 0x08) << 12;

        for(count=PAD_DELAY; count; count--)
        {
            PDR1 = 0x40;
        }
        temp |= (PDR1 & 0x0F) << 8;

        for(count=PAD_DELAY; count; count--)
        {
            PDR1 = 0x20;
        }
        temp |= (PDR1 & 0x0F) << 4;

        for(count=PAD_DELAY; count; count--)
        {
            PDR1 = 0x00;
        }
        temp |= (PDR1 & 0x0F) << 0;
    }
    else
    { /* Read input from pad2 */
        for(count=PAD_DELAY; count; count--)
        {
            PDR2 = 0x60;
        }
        temp = (PDR2 & 0x08) << 12;

        for(count=PAD_DELAY; count; count--)
        {
            PDR2 = 0x40;
        }
        temp |= (PDR2 & 0x0F) << 8;

        for(count=PAD_DELAY; count; count--)
        {
            PDR2 = 0x20;
        }
        temp |= (PDR2 & 0x0F) << 4;

        for(count=PAD_DELAY; count; count--)
        {
            PDR2 = 0x00;
        }
        temp |= (PDR2 & 0x0F) << 0;
    }

    temp ^= 0x8FFF;

    /* Use only A-C, Start and direction keys, because other keys may cause problems when using yabause. */
    temp &= PAD_A | PAD_B | PAD_C | PAD_START | PAD_UP | PAD_DOWN | PAD_LEFT | PAD_RIGHT;

    return temp;
}
static int InitPad(void)
{
    int pad_connected = 3;
    unsigned char temp;
    int count;

    DDR1    = 0x60; /* TH,TR out, TL,D3-D0 in */
    DDR2    = 0x60; /* TH,TR out, TL,D3-D0 in */
    IOSEL   = 0x03; /* Manual mode for both ports */
    EXLE    = 0x00;


    /** Test pad1 **/
    PDR1 = 0x60;
    for(count=0; count < 16; count++)
    {
        PDR1 = 0x60;
    }
    temp = PDR1;

    if((temp&0x1) != 0x00)
    { /* Pad1 is disconnected. */
        pad_connected -= 1;
    }


    /** Test pad2 **/
    PDR2 = 0x60;
    for(count=0; count < 16; count++)
    {
        PDR2 = 0x60;
    }
    temp = PDR2;

    if((temp&0x1) != 0x00)
    { /* Pad2 is disconnected. */
        pad_connected -= 2;
    }

    /* Wait for the user to release pad buttons. */
    while(GetPadStatus(pad_connected) != 0x0000) {;}

    return pad_connected;
}

static void ExitToMultiplayer(void)
{
    /* Exit to multiplayer. */
    (**(volatile void(**)(void))0x600026C)();
}

#define PAD_CHECK_COUNT (32*1024 - 1)
#define TOGGLE_COUNT    (PAD_CHECK_COUNT * 10)
static uint8_t RecvByteAnimate(int pad_connected)
{
    uint32_t i;
    Color_e color = GREEN;

    while(1)
    {
        color = (color == GREEN ? BLUE : GREEN);
        SetScreenColor(color);

        for(i=TOGGLE_COUNT; i; i--)
        {
            /* Check I/O request from PC. */
            if((USB_FLAGS & USB_RXF) == 0)
                return USB_FIFO;

            /* Check if user wants to exit. */
            if((i & PAD_CHECK_COUNT) == 0)
            {
                uint16_t pad_stat = GetPadStatus(pad_connected);
                if(pad_stat != 0x0000)
                {
                    if(pad_stat & PAD_START)
                    {
                        return USBDC_FUNC_EXIT;
                    }
                    else
                    {
                        ExitToMultiplayer();
                    }
                }
            }
        }
    }

    return 0; // For grumpy compiler
}

static uint8_t RecvByte(void)
{
    while ((USB_FLAGS & USB_RXF) != 0) ;
    return USB_FIFO;
}

static uint32_t RecvDword(void)
{
    uint32_t tmp = RecvByte();
    tmp = (tmp << 8) | RecvByte();
    tmp = (tmp << 8) | RecvByte();
    tmp = (tmp << 8) | RecvByte();

    return tmp;
}

static void SendByte(uint8_t byte)
{
    while ((USB_FLAGS & USB_TXE) != 0) ;
    USB_FIFO = byte;
}

static void DoDownload(void)
{
    uint8_t    *pData;
    uint32_t    len, ii;
    uint8_t     tmp;
    crc_t       checksum = crc_init();

    SetScreenColor(ORANGE);

    pData = (uint8_t*)RecvDword();

    len = RecvDword();

    if(len == 2)
    {
        ii = *((uint16_t*)pData);

        tmp = (ii >>  8) & 0xFF; SendByte(tmp); checksum = crc_update_byte(checksum, tmp);
        tmp = (ii >>  0) & 0xFF; SendByte(tmp); checksum = crc_update_byte(checksum, tmp);
    }
    else if(len == 4)
    {
        ii = *((uint32_t*)pData);

        tmp = (ii >> 24) & 0xFF; SendByte(tmp); checksum = crc_update_byte(checksum, tmp);
        tmp = (ii >> 16) & 0xFF; SendByte(tmp); checksum = crc_update_byte(checksum, tmp);
        tmp = (ii >>  8) & 0xFF; SendByte(tmp); checksum = crc_update_byte(checksum, tmp);
        tmp = (ii >>  0) & 0xFF; SendByte(tmp); checksum = crc_update_byte(checksum, tmp);
    }
    else
    {
        for (ii = 0; ii < len; ++ii)
        {
            while ((USB_FLAGS & USB_TXE) != 0) ;
            USB_FIFO = pData[ii];
        }

        checksum = crc_update(checksum, pData, len);
    }

    checksum = crc_finalize(checksum);

    SendByte(checksum);
}

static void DoUpload(void)
{
    uint8_t    *pData;
    uint32_t    len, ii;
    uint8_t     tmp;
    crc_t       checksum = crc_init();

    SetScreenColor(ORANGE);

    pData = (uint8_t*)RecvDword();

    len = RecvDword();

    if(len == 2)
    {
        tmp = RecvByte(); ii =             tmp; checksum = crc_update_byte(checksum, tmp);
        tmp = RecvByte(); ii = (ii << 8) | tmp; checksum = crc_update_byte(checksum, tmp);

        *((uint16_t*)pData) = (uint16_t)ii;
    }
    else if(len == 4)
    {
        tmp = RecvByte(); ii =             tmp; checksum = crc_update_byte(checksum, tmp);
        tmp = RecvByte(); ii = (ii << 8) | tmp; checksum = crc_update_byte(checksum, tmp);
        tmp = RecvByte(); ii = (ii << 8) | tmp; checksum = crc_update_byte(checksum, tmp);
        tmp = RecvByte(); ii = (ii << 8) | tmp; checksum = crc_update_byte(checksum, tmp);

        *((uint32_t*)pData) = (uint32_t)ii;
    }
    else
    {
        for (ii = 0; ii < len; ++ii)
        {
            // inlining is 20K/s faster
            while ((USB_FLAGS & USB_RXF) != 0) ;
            pData[ii] = USB_FIFO;
        }

        checksum = crc_update(checksum, pData, len);
    }

    checksum = crc_finalize(checksum);

    if (checksum != RecvByte())
    {
        SendByte(0x1);
        SignalError();
    }
    else
    {
        SendByte(0);
    }
}

static void DoExecute(void)
{
    /* Read address, execute call. */
    void (*pFun)(void);

    pFun = (void(*)(void))RecvDword();
    (*pFun)();
}

/**
 * Send address to memory not used by this program.
 * Purpose : being able to upload & execute program over this program.
 * Example: if running from HRAM, should send LRAM address.
 * Since this program is running from ROM, send null address
 * in order to inform that any place is OK for upload&exec.
**/
static void DoSendBufferAddress(void)
{
    SendByte(0x00);
    SendByte(0x00);
    SendByte(0x00);
    SendByte(0x00);
}

/**
 * Copy, then execute data.
 * Purpose : being able to upload & execute program over this program.
 * Since this program is running from ROM, this function is useless.
 * Hence, receive parameters and do nothing.
**/
static void DoCopyExec(void)
{
    RecvDword(); /* Address where copying data from. */
    RecvDword(); /* Data length. */
    RecvDword(); /* Address where copying data to. */
}

/* Setup back screen and color entries, turns video on. */
static void InitVideo(void)
{
    TVMD = 0x8110;
    BGON  = 0;
    LCTAU = 0;
    LCTAL = 0;
    BKTAL = 0;
    BKTAU = 0;
    SetScreenColor(BLACK);
}

int usb_dev_cart_check_io(void)
{
    uint8_t command;
    int pad_connected;

    InitVideo();
    SetScreenColor(ORANGE); // <- in order to indicate to release keys
    pad_connected = InitPad();
    if(pad_connected == 0)
    {
        SignalError(); // No pad connected
    }

    // Configure SCU A-bus settings
    // Disabling A-bus refresh adds ~20KB/s
    AREF = 0;
    // Removing wait states doubles speed
    ASR0 = 0;

    while(1)
    {
        SetScreenColor(GREEN);
        command = RecvByteAnimate(pad_connected);
        switch (command)
        {
        case USBDC_FUNC_DOWNLOAD:
            DoDownload();
            break;

        case USBDC_FUNC_UPLOAD:
            DoUpload();
            break;

        case USBDC_FUNC_EXEC:
            DoExecute();
            InitVideo();
            break;

        case USBDC_FUNC_GET_BUFF_ADDR:
            DoSendBufferAddress();
            break;

        case USBDC_FUNC_COPYEXEC:
            DoCopyExec();
            break;

        default:
            SignalError(); // Unknown command
            break;

        case USBDC_FUNC_EXIT:
            return 1;
            break;
        }

    }

    return 0;
}
