/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 *   Mupen64plus - cheat.c                                                 *
 *   Mupen64Plus homepage: https://mupen64plus.org/                        *
 *   Copyright (C) 2009-2010 Richard Goedeken                              *
 *   Copyright (C) 2010 Rhett Osborne (spinout)                            *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.          *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "cheat.h"
#include "core_interface.h"
#include "m64p_types.h"
#include "main.h"

/* local definitions */
#define CHEAT_FILE	"mupencheat.txt"

typedef struct {
   int    address;
   int   *variables;
   char **variable_names;
   int    var_to_use;
   int    var_count;
} cheat_code;

typedef struct _sCheatInfo {
  int                 Number;
  int                 Count;
  int                 VariableLine;
  const char         *Name;
  const char         *Description;
  cheat_code         *Codes;
  struct _sCheatInfo *Next;
  } sCheatInfo;

/* local variables */
static m64p_rom_header    l_RomHeader;
static char              *l_IniText = NULL;
static char              *l_CheatGameName = NULL;
static sCheatInfo        *l_CheatList = NULL;
static int                l_CheatCodesFound = 0;
static int                l_RomFound = 0;

/*********************************************************************************************************
 *  Static (Local) functions
 */

static int isSpace(char ch)
{
    return (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n');
}

/* Find cheat code */
static sCheatInfo *CheatFindCode(int Number)
{
    sCheatInfo *pCur = l_CheatList;
    while (pCur != NULL)
    {
        if (pCur->Number == Number) break;
        pCur = pCur->Next;
    }
    return pCur;
}


/* Activate a code */
static void CheatActivate(sCheatInfo *pCheat)
{
    int i;

    /* Get a m64p_cheat_code object */
    m64p_cheat_code * code = (m64p_cheat_code*) calloc(pCheat->Count, sizeof(m64p_cheat_code));
    if (code == NULL)
    {
        DebugMessage(M64MSG_WARNING, "could not allocate memory for code '%s'", pCheat->Name);
        return;
    }
    /* Fill in members */
    for (i = 0; i < pCheat->Count; i++)
    {
        code[i].address = pCheat->Codes[i].address;
        code[i].value = pCheat->Codes[i].variables[pCheat->Codes[i].var_to_use];
    }
    /* Enable cheat */
    if (CoreAddCheat(pCheat->Name, code, pCheat->Count) != M64ERR_SUCCESS)
    {
        DebugMessage(M64MSG_WARNING, "CoreAddCheat() failed for cheat code %i (%s)", pCheat->Number, pCheat->Name);
        free(code);
        return;
    }

    free(code);
    DebugMessage(M64MSG_STATUS, "activated cheat code %i: %s", pCheat->Number, pCheat->Name);
}

static void CheatFreeAll(void)
{
    if (l_IniText != NULL)
        free(l_IniText);
    l_IniText = NULL;

    sCheatInfo *pCur = l_CheatList;
    while (pCur != NULL)
    {
        sCheatInfo *pNext = pCur->Next;
        if (pCur->Codes != NULL)
        {
            int i;
            for (i=0; i < pCur->Count; i++)
            {
                if (pCur->Codes[i].variables != NULL)
                    free(pCur->Codes[i].variables);
                if (pCur->Codes[i].variable_names != NULL)
                    free(pCur->Codes[i].variable_names);
            }
            free(pCur->Codes);
        }
        free(pCur);
        pCur = pNext;
    }

    l_CheatList = NULL;
}

/* Append new code */
static sCheatInfo * NewCode(char *CheatName, int CheatNum)
{
    /* allocate memory for a new sCheatInfo struct */
    sCheatInfo *pNew = (sCheatInfo *) malloc(sizeof(sCheatInfo));
    if (pNew == NULL) return NULL;

    /* fill in the data members */
    pNew->Number = CheatNum;
    pNew->Count = 0;
    pNew->VariableLine = -1;
    pNew->Name = CheatName;
    pNew->Description = NULL;
    pNew->Codes = NULL;
    pNew->Next = NULL;

    l_CheatCodesFound++;

    /* stick it at the end of the list */
    if (l_CheatList == NULL)
    {
        l_CheatList = pNew;
        return pNew;
    }
    sCheatInfo *pLast = l_CheatList;
    while (pLast->Next != NULL) pLast = pLast->Next;
    pLast->Next = pNew;
    return pNew;
}

static void CheatAddVariables(cheat_code * Code, char *varlist)
{
    /* needs to be more verbose? */
    Code->variables = NULL;
    Code->variable_names = NULL;
    Code->var_count = 0;
    while (*varlist != 0)
    {
        if ((Code->variables = (int*) realloc(Code->variables, sizeof(int) * (Code->var_count + 1))) == NULL)
            return;
        if ((Code->variable_names = (char**) realloc(Code->variable_names, sizeof(char*) * (Code->var_count + 1))) == NULL)
            return;
        if (sscanf(varlist, "%04X", &Code->variables[Code->var_count]) != 1)
            Code->variables[Code->var_count] = 0;
        if (strchr(varlist, '"') == NULL)
            return;
        Code->variable_names[Code->var_count] = strchr(varlist, '"') + 1;
        if ((varlist = strchr(Code->variable_names[Code->var_count], '"')) == NULL)
            return;
        *varlist++ = 0;
        if (*varlist == ',')
            varlist++;
        Code->var_count++;
    }
}

/*********************************************************************************************************
* global functions
*/

static void ReadCheats(char *RomSection)
{
    sCheatInfo *curr_code = NULL;
    const char *romdbpath = ConfigGetSharedDataFilepath(CHEAT_FILE);
    if (romdbpath == NULL)
    {
        DebugMessage(M64MSG_WARNING, "cheat code database file '%s' not found.", CHEAT_FILE);
        return;
    }

    /* read the INI file into a new buffer */
    FILE *fPtr = NULL;
    fPtr = fopen(romdbpath, "rb");
    if (fPtr == NULL)
    {   
        DebugMessage(M64MSG_WARNING, "Couldn't open cheat code database file '%s'.", romdbpath);
        return;
    }
    fseek(fPtr, 0L, SEEK_END);
    long IniLength = ftell(fPtr);
    fseek(fPtr, 0L, SEEK_SET);
    l_IniText = (char *) malloc(IniLength + 1);
    if (l_IniText == NULL)
    {
        DebugMessage(M64MSG_WARNING, "Couldn't allocate %li bytes of memory to read cheat file.", IniLength);
        fclose(fPtr);
        return;
    }
    if (fread(l_IniText, 1, IniLength, fPtr) != IniLength)
    {
        DebugMessage(M64MSG_WARNING, "Couldn't read %li bytes from cheat file.", IniLength);
        free(l_IniText);
        l_IniText = NULL;
        fclose(fPtr);
        return;
    }
    fclose(fPtr);
    l_IniText[IniLength] = 0; /* null-terminate the text data */

    /* parse lines from cheat database */
    char *curline = NULL;
    char *nextline = l_IniText;

    while(nextline != NULL && *nextline != 0)
    {
        curline = nextline;
        /* get pointer to next line and NULL-terminate the current line */
        nextline = strchr(curline, '\n');
        if (nextline != NULL)
        {
            *nextline = 0;
            nextline++;
        }

        /* remove leading and trailing white space */
        while(isSpace(*curline)) curline++;
        char *endptr = curline + strlen(curline) - 1;
        while(isSpace(*endptr)) *endptr-- = 0;

        /* ignore line if comment or empty */
        if (*curline == '#' || strncmp(curline, "//", 2) == 0 || *curline == 0)
            continue;

        /* handle beginning of new rom section */
        if (strncmp(curline, "crc ", 4) == 0)
        {
            /* if we have already found cheats for the given ROM file, then exit upon encountering a new ROM section */
            if (l_RomFound && (l_CheatGameName != NULL || l_CheatList != NULL))
                return;
            /* else see if this Rom Section matches */
            if (strcmp(curline+4, RomSection) == 0)
                l_RomFound = 1;
            continue;
        }

        /* if we haven't found the specified ROM section, then continue looking */
        if (!l_RomFound)
            continue;

        /* Game name */
        if (strncmp(curline, "gn ", 3) == 0)
        {
            l_CheatGameName = curline+3;
            continue;
        }

        /* code name */
        if (strncmp(curline, "cn ", 3) == 0)
        {
            curr_code = NewCode(curline + 3, l_CheatCodesFound);
            if (curr_code == NULL)
                DebugMessage(M64MSG_WARNING, "error getting new code (%s)", curline+3);
            continue;
        }
        
        /* if curr_code is NULL, don't do these checks */
        if (curr_code == NULL)
            continue;

        /* code description */
        if (strncmp(curline, "cd ", 3) == 0)
        {
            curr_code->Description = curline+3;
            continue;
        }

        /* code line */
        int address;
        if (sscanf(curline, "%8X %*s", &address) == 1)
        {
            curr_code->Codes = (cheat_code*) realloc(curr_code->Codes, sizeof(cheat_code) * (curr_code->Count + 1));
            if (strncmp(curline+9, "????", 4) == 0)
            {
                curr_code->Codes[curr_code->Count].var_count = 0;
                CheatAddVariables(&curr_code->Codes[curr_code->Count], curline+14);
                curr_code->VariableLine = curr_code->Count;
            }
            else
            {
                int var;
                curr_code->Codes[curr_code->Count].var_count = 1;
                curr_code->Codes[curr_code->Count].variables = (int*) malloc(sizeof(int));
                if(curr_code->Codes[curr_code->Count].variables == NULL)
                {
                    DebugMessage(M64MSG_WARNING, "couldn't allocate memory; ignoring line: '%s'", curline);
                    continue;
                }
                if (sscanf(curline+9, "%04X", &var) != 1)
                    var = 0;
                curr_code->Codes[curr_code->Count].variables[0] = var;
                curr_code->Codes[curr_code->Count].variable_names = NULL;
            }
            curr_code->Codes[curr_code->Count].var_to_use = 0;
            curr_code->Codes[curr_code->Count].address = address;
            curr_code->Count++;
            continue;
        }

        /* otherwise we don't know what this line is */
        DebugMessage(M64MSG_WARNING, "unrecognized line in cheat file: '%s'", curline);
    }

}

void CheatStart(eCheatMode CheatMode, char *CheatNumList)
{
    /* if cheat codes are disabled, then we don't have to do anything */
    if (CheatMode == CHEAT_DISABLE || (CheatMode == CHEAT_LIST && strlen(CheatNumList) == 0))
    {
        DebugMessage(M64MSG_STATUS, "Cheat codes disabled.");
        return;
    }

    /* get the ROM header for the currently loaded ROM image from the core */
    if ((*CoreDoCommand)(M64CMD_ROM_GET_HEADER, sizeof(l_RomHeader), &l_RomHeader) != M64ERR_SUCCESS)
    {
        DebugMessage(M64MSG_WARNING, "couldn't get ROM header information from core library");
        return;
    }

    /* generate section name from ROM's CRC and country code */
    char RomSection[24];
    sprintf(RomSection, "%08X-%08X-C:%X", sl(l_RomHeader.CRC1), sl(l_RomHeader.CRC2), l_RomHeader.Country_code & 0xff);

    /* parse through the cheat INI file and load up any cheat codes found for this ROM */
    ReadCheats(RomSection);
    if (!l_RomFound || l_CheatCodesFound == 0)
    {
        DebugMessage(M64MSG_WARNING, "no cheat codes found for ROM image '%.20s'", l_RomHeader.Name);
        CheatFreeAll();
        return;
    }

    /* handle the list command */
    if (CheatMode == CHEAT_SHOW_LIST)
    {
        DebugMessage(M64MSG_INFO, "%i cheat code(s) found for ROM '%s'", l_CheatCodesFound, l_CheatGameName);
        sCheatInfo *pCur = l_CheatList;
        while (pCur != NULL)
        {
            if (pCur->Description == NULL)
                DebugMessage(M64MSG_INFO, "   %i: %s", pCur->Number, pCur->Name);
            else
                DebugMessage(M64MSG_INFO, "   %i: %s (%s)", pCur->Number, pCur->Name, pCur->Description);
            if(pCur->VariableLine != -1)
            {
                int i;
                for (i = 0; i < pCur->Codes[pCur->VariableLine].var_count; i++)
                    DebugMessage(M64MSG_INFO, "      %i: %s", i, pCur->Codes[pCur->VariableLine].variable_names[i]);
            }
            pCur = pCur->Next;
        }
        CheatFreeAll();
        return;
    }

    /* handle all cheats enabled mode */
    if (CheatMode == CHEAT_ALL)
    {
        sCheatInfo *pCur = l_CheatList;
        while (pCur != NULL)
        {
            CheatActivate(pCur);
            pCur = pCur->Next;
        }
        CheatFreeAll();
        return;
    }

    /* handle list of cheats enabled mode */
    if (CheatMode == CHEAT_LIST)
    {
        int option, number;
        char *cheat_next;
        sCheatInfo *pCheat;
        while(CheatNumList != NULL && *CheatNumList)
        {
            if ((cheat_next = strchr(CheatNumList, ',')) != NULL)
            {
                *cheat_next = 0;
                cheat_next ++;
            }

            if (strchr(CheatNumList, '-') != NULL)
            {
                sscanf(CheatNumList, "%i-%i", &number, &option); /* option */
            }
            else
            {
                option=0;
                sscanf(CheatNumList, "%i", &number);
            }

            pCheat = CheatFindCode(number);
            if (pCheat == NULL)
                DebugMessage(M64MSG_WARNING, "invalid cheat code number %i", number);
            else
            {
                if (pCheat->VariableLine != -1 && pCheat->Count > pCheat->VariableLine && option < pCheat->Codes[pCheat->VariableLine].var_count)
                    pCheat->Codes[pCheat->VariableLine].var_to_use = option;
                CheatActivate(pCheat);
            }

            CheatNumList = cheat_next;
        }
        CheatFreeAll();
        
        return;
    }

    /* otherwise the mode is invalid */
    DebugMessage(M64MSG_WARNING, "internal error; invalid CheatMode in CheatStart()");
    
    return;
}

