/* Cheat Support */
#include <stdbool.h>

#define MAX_CHEATS (150)
#define MAX_DESC_LENGTH (63)

typedef struct
{
    char code[12];
    char text[MAX_DESC_LENGTH];
    uint8_t enable;
    uint16_t data;
    uint16_t old;
    uint32_t address;
    uint8_t *prev;
} CHEATENTRY;

static int maxcheats = 0;
static int maxROMcheats = 0;
static int maxRAMcheats = 0;
static CHEATENTRY cheatlist[MAX_CHEATS];
static uint8_t cheatIndexes[MAX_CHEATS];
static char ggvalidchars[] = "ABCDEFGHJKLMNPRSTVWXYZ0123456789";
static char arvalidchars[] = "0123456789ABCDEF";

static uint32_t decode_cheat(char *string, int index)
{
    char *p;
    int i,n;
    uint32_t len = 0;
    uint32_t address = 0;
    uint16_t data = 0;
    uint8_t ref = 0;

    if ((system_hw & SYSTEM_PBC) == SYSTEM_MD){
        /*If system is Genesis-based*/

        /*Game-Genie*/
        if ((strlen(string) >= 9) && (string[4] == '-'))
        {
            for (i = 0; i < 8; i++)
            {
                if (i == 4) string++;
                p = strchr (ggvalidchars, *string++);
                if (!p)
                    return 0;
                n = p - ggvalidchars;
                switch (i)
                {
                    case 0:
                        data |= n << 3;
                        break;
                    case 1:
                        data |= n >> 2;
                        address |= (n & 3) << 14;
                        break;
                    case 2:
                        address |= n << 9;
                        break;
                    case 3:
                        address |= (n & 0xF) << 20 | (n >> 4) << 8;
                        break;
                    case 4:
                        data |= (n & 1) << 12;
                        address |= (n >> 1) << 16;
                        break;
                    case 5:
                        data |= (n & 1) << 15 | (n >> 1) << 8;
                        break;
                    case 6:
                        data |= (n >> 3) << 13;
                        address |= (n & 7) << 5;
                        break;
                    case 7:
                        address |= n;
                        break;
                }
            }
            /* code length */
            len = 9;
        }

        /*Patch and PAR*/
        else if ((strlen(string) >=9) && (string[6] == ':'))
        {
            /* decode 24-bit address */
            for (i=0; i<6; i++)
            {
                p = strchr (arvalidchars, *string++);
                if (!p)
                    return 0;
                n = (p - arvalidchars) & 0xF;
                address |= (n << ((5 - i) * 4));
            }
            /* decode 16-bit data */
            string++;
            for (i=0; i<4; i++)
            {
                p = strchr (arvalidchars, *string++);
                if (!p)
                    break;
                n = (p - arvalidchars) & 0xF;
                data |= (n << ((3 - i) * 4));
            }
            /* code length */
            len = 11;
        }
    } else {
        /*If System is Master-based*/

        /*Game Genie*/
        if ((strlen(string) >=7) && (string[3] == '-'))
        {
            /* decode 8-bit data */
            for (i=0; i<2; i++)
            {
                p = strchr (arvalidchars, *string++);
                if (!p)
                    return 0;
                n = (p - arvalidchars) & 0xF;
                data |= (n << ((1 - i) * 4));
            }

            /* decode 16-bit address (low 12-bits) */
            for (i=0; i<3; i++)
            {
                if (i==1) string++; /* skip separator */
                p = strchr (arvalidchars, *string++);
                if (!p)
                    return 0;
                n = (p - arvalidchars) & 0xF;
                address |= (n << ((2 - i) * 4));
            }
            /* decode 16-bit address (high 4-bits) */
            p = strchr (arvalidchars, *string++);
            if (!p)
                return 0;
            n = (p - arvalidchars) & 0xF;
            n ^= 0xF; /* bits inversion */
            address |= (n << 12);
            /* Optional: decode reference 8-bit data */
            if (*string=='-')
            {
                for (i=0; i<2; i++)
                {
                    string++; /* skip separator and 2nd digit */
                    p = strchr (arvalidchars, *string++);
                    if (!p)
                        return 0;
                    n = (p - arvalidchars) & 0xF;
                    ref |= (n << ((1 - i) * 4));
                }
                ref = (ref >> 2) | ((ref & 0x03) << 6); /* 2-bit right rotation */
                ref ^= 0xBA; /* XOR */
                /* code length */
                len = 11;
            }
            else
            {
                /* code length */
                len = 7;
            }
        }

        /*Action Replay*/
        else if ((strlen(string) >=9) && (string[4] == '-')){
            string+=2;
            /* decode 16-bit address */
            for (i=0; i<4; i++)
            {
                p = strchr (arvalidchars, *string++);
                if (!p)
                    return 0;
                n = (p - arvalidchars) & 0xF;
                address |= (n << ((3 - i) * 4));
                if (i==1) string++;
            }
            /* decode 8-bit data */
            for (i=0; i<2; i++)
            {
                p = strchr (arvalidchars, *string++);
                if (!p)
                    return 0;
                n = (p - arvalidchars) & 0xF;
                data |= (n << ((1 - i) * 4));
            }
            /* code length */
            len = 9;
        }

        /*Fusion RAM*/
        else if ((strlen(string) >=7) && (string[4] == ':'))
        {
            /* decode 16-bit address */
            for (i=0; i<4; i++)
            {
                p = strchr (arvalidchars, *string++);
                if (!p)
                    return 0;
                n = (p - arvalidchars) & 0xF;
                address |= (n << ((3 - i) * 4));
            }
            /* decode 8-bit data */
            string++;
            for (i=0; i<2; i++)
            {
                p = strchr (arvalidchars, *string++);
                if (!p)
                    return 0;
                n = (p - arvalidchars) & 0xF;
                data |= (n << ((1 - i) * 4));
            }
            /* code length */
            len = 7;
        }

        /*Fusion ROM*/
        else if ((strlen(string) >=9) && (string[6] == ':'))
        {
            /* decode reference 8-bit data */
            for (i=0; i<2; i++)
            {
                p = strchr (arvalidchars, *string++);
                if (!p)
                    return 0;
                n = (p - arvalidchars) & 0xF;
                ref |= (n << ((1 - i) * 4));
            }
            /* decode 16-bit address */
            for (i=0; i<4; i++)
            {
                p = strchr (arvalidchars, *string++);
                if (!p)
                    return 0;
                n = (p - arvalidchars) & 0xF;
                address |= (n << ((3 - i) * 4));
            }
            /* decode 8-bit data */
            string++;
            for (i=0; i<2; i++)
            {
                p = strchr (arvalidchars, *string++);
                if (!p)
                    return 0;
                n = (p - arvalidchars) & 0xF;
                data |= (n << ((1 - i) * 4));
            }
            /* code length */
            len = 9;
        }
        /* convert to 24-bit Work RAM address */
        if (address >= 0xC000)
            address = 0xFF0000 | (address & 0x1FFF);
    }
    /* Valid code found ? */
    if (len)
    {
        /* update cheat address & data values */
        cheatlist[index].address = address;
        cheatlist[index].data = data;
        cheatlist[index].old = ref;
    }
    /* return code length (0 = invalid) */
    return len;
}

static void apply_cheats(void)
{
    uint8_t *ptr;
    int i;
    /* clear ROM&RAM patches counter */
    maxROMcheats = maxRAMcheats = 0;

    for (i = 0; i < maxcheats; i++)
    {
        if (cheatlist[i].enable)
        {
            /* detect Work RAM patch */
            if (cheatlist[i].address >= 0xFF0000)
            {
                /* add RAM patch */
                cheatIndexes[maxRAMcheats++] = i;
            }

            /* check if Mega-CD game is running */
            else if ((system_hw == SYSTEM_MCD) && !scd.cartridge.boot)
            {
                /* detect PRG-RAM patch (Sub-CPU side) */
                if (cheatlist[i].address < 0x80000)
                {
                    /* add RAM patch */
                    cheatIndexes[maxRAMcheats++] = i;
                }

                /* detect Word-RAM patch (Main-CPU side)*/
                else if ((cheatlist[i].address >= 0x200000) && (cheatlist[i].address < 0x240000))
                {
                    /* add RAM patch */
                    cheatIndexes[maxRAMcheats++] = i;
                }
            }

            /* detect cartridge ROM patch */
            else if (cheatlist[i].address < cart.romsize)
            {
                if ((system_hw & SYSTEM_PBC) == SYSTEM_MD)
                {
                    /* patch ROM data */
                    cheatlist[i].old = *(uint16_t *)(cart.rom + (cheatlist[i].address & 0xFFFFFE));
                    *(uint16_t *)(cart.rom + (cheatlist[i].address & 0xFFFFFE)) = cheatlist[i].data;
                }
                else
                {
                    /* add ROM patch */
                    maxROMcheats++;
                    cheatIndexes[MAX_CHEATS - maxROMcheats] = i;
                    /* get current banked ROM address */
                    ptr = &z80_readmap[(cheatlist[i].address) >> 10][cheatlist[i].address & 0x03FF];
                    /* check if reference matches original ROM data */
                    if (((uint8_t)cheatlist[i].old) == *ptr)
                    {
                        /* patch data */
                        *ptr = cheatlist[i].data;
                        /* save patched ROM address */
                        cheatlist[i].prev = ptr;
                    }
                    else
                    {
                        /* no patched ROM address yet */
                        cheatlist[i].prev = NULL;
                    }
                }
            }
        }
    }
}

static void clear_cheats(void)
{
    int i;

    /* no ROM patches with Mega-CD games */
    if ((system_hw == SYSTEM_MCD) && !scd.cartridge.boot)
        return;

    /* disable cheats in reversed order in case the same address is used by multiple ROM patches */
    i = maxcheats;
    while (i > 0)
    {
        if (cheatlist[i-1].enable)
        {
            if (cheatlist[i-1].address < cart.romsize)
            {
                if ((system_hw & SYSTEM_PBC) == SYSTEM_MD)
                {
                    /* restore original ROM data */
                    *(uint16_t *)(cart.rom + (cheatlist[i-1].address & 0xFFFFFE)) = cheatlist[i-1].old;
                }
                else
                {
                    /* check if previous banked ROM address has been patched */
                    if (cheatlist[i-1].prev != NULL)
                    {
                        /* restore original data */
                        *cheatlist[i-1].prev = cheatlist[i-1].old;
                        /* no more patched ROM address */
                        cheatlist[i-1].prev = NULL;
                    }
                }
            }
        }
        i--;
    }
}

/****************************************************************************
 * RAMCheatUpdate
 *
 * Apply RAM patches (this should be called once per frame)
 *
 ****************************************************************************/
static void RAMCheatUpdate(void)
{
    uint8_t *base;
    uint32_t mask;
    int index, cnt = maxRAMcheats;

    while (cnt)
    {
        /* get cheat index */
        index = cheatIndexes[--cnt];

        /* detect destination RAM */
        switch ((cheatlist[index].address >> 20) & 0xf)
        {
            case 0x0: /* Mega-CD PRG-RAM (512 KB) */
                base = scd.prg_ram;
                mask = 0x7fffe;
                break;

            case 0x2: /* Mega-CD 2M Word-RAM (256 KB) */
                base = scd.word_ram_2M;
                mask = 0x3fffe;
                break;

            default: /* Work-RAM (64 KB) */
                base = work_ram;
                mask = 0xfffe;
                break;
        }

        /* apply RAM patch */
        // TODO: Investigate. Some PAR cheats for Genesis don't work otherwise.
        // e.g. Sonic The Hedgehog 2 (World) (Rev A).md (MD5 9feeb724052c39982d432a7851c98d3e) using Invincibility (Sonic only) code FFB02B:0002
        // Fixes possible endianness issue, also does not invoke pointer typecasting UB
        bool isSega16bit = ((system_hw & SYSTEM_PBC) == SYSTEM_MD) || (system_hw == SYSTEM_MCD);
        //if (cheatlist[index].data & 0xFF00)
        if (isSega16bit ? cheatlist[index].data & 0x00FF : cheatlist[index].data & 0xFF00)
        {
            /* word patch */
            unsigned addr = cheatlist[index].address & mask;
            base[addr] = cheatlist[index].data & 0xFF;
            base[addr + 1] = (cheatlist[index].data & 0xFF00) >> 8;
            //*(uint16_t *)(base + (cheatlist[index].address & mask)) = cheatlist[index].data;
        }
        else
        {
            /* byte patch */
            mask |= 1;
            base[cheatlist[index].address & mask] = cheatlist[index].data;
        }
    }
}

/****************************************************************************
 * ROMCheatUpdate
 *
 * Apply ROM patches (this should be called each time banking is changed)
 *
 ****************************************************************************/
void ROMCheatUpdate(void)
{
    int index, cnt = maxROMcheats;
    uint8_t *ptr;

    while (cnt)
    {
        /* get cheat index */
        index = cheatIndexes[MAX_CHEATS - cnt];

        /* check if previous banked ROM address was patched */
        if (cheatlist[index].prev != NULL)
        {
            /* restore original data */
            *cheatlist[index].prev = cheatlist[index].old;

            /* no more patched ROM address */
            cheatlist[index].prev = NULL;
        }

        /* get current banked ROM address */
        ptr = &z80_readmap[(cheatlist[index].address) >> 10][cheatlist[index].address & 0x03FF];

        /* check if reference exists and matches original ROM data */
        if (!cheatlist[index].old || ((uint8_t)cheatlist[index].old) == *ptr)
        {
            /* patch data */
            *ptr = cheatlist[index].data;

            /* save patched ROM address */
            cheatlist[index].prev = ptr;
        }

        /* next ROM patch */
        cnt--;
    }
}
