/*
 * COPYRIGHT:             See COPYING in the top level directory
 * PROJECT:               Header preprocessor
 * PURPOSE:               Generates header files from other header files
 * PROGRAMMER;            Timo Kreuzer
 *
 */

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

//#define DBG 1

#if DBG
#define trace printf
#else
#define trace if (0) printf
#endif

typedef struct _DEFINE
{
    struct _DEFINE *pNext;
    int val;
    char *pszName;
    unsigned int cchName;
    char *pszValue;
    unsigned int cchValue;
    char achBuffer[1];
} DEFINE, *PDEFINE;

DEFINE *gpDefines = 0;
int iLine;
const char *gpszCurFile;

char*
convert_path(const char* origpath)
{
    char* newpath;
    int i;

    newpath = strdup(origpath);

    i = 0;
    while (newpath[i] != 0)
    {
#ifdef UNIX_PATHS
        if (newpath[i] == '\\')
        {
            newpath[i] = '/';
        }
#else
#ifdef DOS_PATHS
        if (newpath[i] == '/')
        {
            newpath[i] = '\\';
        }
#endif
#endif
        i++;
    }
    return newpath;
}

char*
GetFolder(const char* pszFullPath)
{
    return ".";
}

void*
LoadFile(const char* pszFileName, size_t* pFileSize)
{
    FILE* file;
    void* pFileData = NULL;
    int iFileSize;

    trace("Loading file...");

    file = fopen(pszFileName, "rb");
    if (!file)
    {
        trace("Could not open file\n");
        return NULL;
    }

    fseek(file, 0L, SEEK_END);
    iFileSize = ftell(file);
    fseek(file, 0L, SEEK_SET);
    *pFileSize = iFileSize;
    trace("ok. Size is %d\n", iFileSize);

    pFileData = malloc(iFileSize + 1);

    if (pFileData != NULL)
    {
        if (iFileSize != fread(pFileData, 1, iFileSize, file))
        {
            free(pFileData);
            pFileData = NULL;
        }
    }
    else
    {
        trace("Could not allocate memory for file\n");
    }

    fclose(file);

    return pFileData;
}


int
error(char *format, ...)
{
    va_list valist;
    int res;
    va_start(valist, format);
    res = vfprintf(stderr, format, valist);
    va_end(valist);
    return res;
}

char*
GetNextChar(const char *psz)
{
    while (*psz == ' ' || *psz == '\t') psz++;
    return (char*)psz;
}

char*
GetNextLine(char *pszLine)
{
    /* Walk to the end of the line */
    while (*pszLine != 13 && *pszLine != 10 && *pszLine != 0) pszLine++;

    /* Skip one CR/LF */
    if (pszLine[0] == 13 && pszLine[1] == 10)
        pszLine += 2;
    else if (pszLine[0] == 13 || pszLine[0] == 10)
        pszLine++;

    if (*pszLine == 0)
    {
        return 0;
    }

    return pszLine;
}

int
strxlen(const char *psz)
{
    int len = 0;
    while (isalnum(*psz) || *psz == '_')
    {
        psz++;
        len++;
    }
    return len;
}

PDEFINE
FindDefine(const char *p, char **pNext)
{
    PDEFINE pDefine;
    int cchName;

    cchName = strxlen(p);
    if (pNext)
        *pNext = (char*)p + cchName;

    /* search for the define in the global list */
    pDefine = gpDefines;
    while (pDefine != 0)
    {
        trace("found a define: %s\n", pDefine->pszName);
        if (pDefine->cchName == cchName)
        {
            if (strncmp(p, pDefine->pszName, cchName) == 0)
            {
                return pDefine;
            }
        }
        pDefine = pDefine->pNext;
    }
    return 0;
}

void
WriteLine(char *pchLine, FILE *fileOut)
{
    char *pch, *pchLineEnd, *pchVariable;
    int len;
    PDEFINE pDefine;

    pchLineEnd = strchr(pchLine, '\n');
    if (pchLineEnd == 0)
        return;

    len = pchLineEnd - pchLine + 1;

    pch = pchLine;
    while (len > 0)
    {
        /* Check if there is a $ variable in the line */
        pchVariable = strchr(pch, '$');
        if (pchVariable && (pchVariable < pchLineEnd))
        {
            /* Write all characters up to the $ */
            fwrite(pch, 1, pchVariable - pch, fileOut);

            /* Try to find the define */
            pDefine = FindDefine(pchVariable + 1, &pch);
            if (pDefine != 0)
            {
                /* We have a define, write the value */
                fwrite(pDefine->pszValue, 1, pDefine->cchValue, fileOut);
            }
            else
            {
                len = strxlen(pchVariable + 1) + 1;
                error("Could not find variable '%.*s'\n", len, pchVariable);
                fwrite(pchVariable, 1, pch - pchVariable, fileOut);
            }

            len = pchLineEnd - pch + 1;
        }
        else
        {
            fwrite(pch, 1, len, fileOut);
            break;
        }
    }
}

int
EvaluateConstant(const char *p, char **pNext)
{
    PDEFINE pDefine;

    pDefine = FindDefine(p, pNext);
    if (!pDefine)
        return 0;

    return pDefine->val;
}

int
EvaluateExpression(char *pExpression, char **pNext)
{
    char *p, *pstart;
    int inv, thisval, val = 0;

    trace("evaluating expression\n");

    pstart = GetNextChar(pExpression);
    if (*pstart != '(')
    {
        error("Parse error: expected '(' \n");
        return -1;
    }

    while (1)
    {
        /* Get the start of the real expression */
        p = pstart;
        if ((p[0] == '&' && p[1] == '&') ||
            (p[0] == '|' && p[1] == '|'))
        {
            p++;
        }
        p = GetNextChar(p + 1);

        /* Check for inversion modifier */
        if (*p == '!')
        {
            inv = 1;
            p = GetNextChar(p + 1);
        }
        else
            inv = 0;

        /* Beginning of a new subexpression? */
        if (*p == '(')
        {
            /* Evaluate subexpression */
            thisval = EvaluateExpression(p, &p);
        }
        else if (isdigit(*p))
        {
            thisval = strtod(p, &p);
            trace("found a num: %d\n", thisval);
        }
        else if (isalpha(*p) || *p == '_')
        {
            thisval = EvaluateConstant(p, &p);
        }
        else
        {
            error("..Parse error, expected '(' or constant in line %d\n",
                  iLine);
            return -1;
        }

        if (inv)
            thisval = !thisval;

        /* Check how to combine the current value */
        if (pstart[0] == '(')
        {
            val = thisval;
        }
        else if (pstart[0] == '&' && pstart[1] == '&')
        {
            val = val && thisval;
        }
        else if (pstart[0] == '&' && pstart[1] != '&')
        {
            val = val & thisval;
        }
        else if (pstart[0] == '|' && pstart[1] == '|')
        {
            trace("found || val = %d, thisval = %d\n", val, thisval);
            val = val || thisval;
        }
        else if (pstart[0] == '|' && pstart[1] != '|')
        {
            val = val | thisval;
        }
        else if (pstart[0] == '+')
        {
            val = val + thisval;
        }
        else
        {
            error("+Parse error: expected '(' or operator in Line %d, got %c\n",
                  iLine, pstart[0]);
            return -1;
        }

        p = GetNextChar(p);

        /* End of current subexpression? */
        if (*p == ')')
        {
            if (pNext)
            {
                *pNext = p + 1;
            }
            return val;
        }

        /* Continue with a new start position */
        pstart = p;
    }

    return val;
}

int
ParseInputFile(const char *pszInFile, FILE *fileOut)
{
    char* pInputData, *pCurrentLine, *p1, *p2;
    size_t cbInFileLenth;
    int iIfLevel, iCopyLevel;

    trace("parsing input file: %s\n", pszInFile);

    /* Set the global file name */
    gpszCurFile = pszInFile;

    /* Load the input file into memory */
    pInputData = LoadFile(pszInFile, &cbInFileLenth);
    if (!pInputData)
    {
        error("Could not load input file %s\n", pszInFile);
        return -1;
    }

    /* Zero terminate the file */
    pInputData[cbInFileLenth] = 0;

    pCurrentLine = pInputData;
    iLine = 1;
    iCopyLevel = iIfLevel = 0;

    /* The main processing loop */
    do
    {
        trace("line %d: ", iLine);

        /* If this is a normal line ... */
        if (pCurrentLine[0] != '$')
        {
            /* Check if we are to copy this line */
            if (iCopyLevel == iIfLevel)
            {
                trace("copying\n");
                WriteLine(pCurrentLine, fileOut);
            }
            else
                trace("skipping\n");

            /* Continue with next line */
            continue;
        }

        /* Check for $endif */
        if (strncmp(pCurrentLine, "$endif", 6) == 0)
        {
            trace("found $endif\n");
            if (iIfLevel <= 0)
            {
                error("Parse error: $endif without $if in %s:%d\n", pszInFile, iLine);
                return -1;
            }
            if (iCopyLevel == iIfLevel)
            {
                iCopyLevel--;
            }
            iIfLevel--;

            /* Continue with next line */
            continue;
        }

        /* The rest is only parsed when we are in a true block */
        if (iCopyLevel < iIfLevel)
        {
            trace("skipping\n");

            /* Continue with next line */
            continue;
        }

        /* Check for $define */
        if (strncmp(pCurrentLine, "$define", 7) == 0)
        {
            PDEFINE pDefine;
            char *pchName, *pchValue;
            size_t cchName, cchValue;

            trace("found $define\n");
            p1 = GetNextChar(pCurrentLine + 7);
            if (*p1 != '(')
            {
                error("Parse error: expected '(' at %s:%d\n",
                      pszInFile, iLine);
                return -1;
            }

            pchName = GetNextChar(p1 + 1);
            cchName = strxlen(pchName);
            p1 = GetNextChar(pchName + cchName);

            /* Check for assignment */
            if (*p1 == '=')
            {
                trace("found $define with assignment\n");
                pchValue = GetNextChar(p1 + 1);
                cchValue = strxlen(pchValue);
                p1 = GetNextChar(pchValue + cchValue);
            }
            else
            {
                pchValue = 0;
                cchValue = 0;
            }

            /* Allocate a DEFINE structure */
            pDefine = malloc(sizeof(DEFINE) + cchName + cchValue + 2);
            if (pDefine == 0)
            {
                error("Failed to allocate %u bytes\n",
                      sizeof(DEFINE) + cchName + cchValue + 2);
                return -1;
            }

            pDefine->pszName = pDefine->achBuffer;
            strncpy(pDefine->pszName, pchName, cchName);
            pDefine->pszName[cchName] = 0;
            pDefine->cchName = cchName;
            pDefine->val = 1;

            if (pchValue != 0)
            {
                pDefine->pszValue = &pDefine->achBuffer[cchName + 1];
                strncpy(pDefine->pszValue, pchValue, cchValue);
                pDefine->pszValue[cchValue] = 0;
                pDefine->cchValue = cchValue;
            }
            else
            {
                pDefine->pszValue = 0;
                pDefine->cchValue = 0;
            }

            /* Insert the new define into the global list */
            pDefine->pNext = gpDefines;
            gpDefines = pDefine;

            /* Check for closing ')' */
            if (*p1 != ')')
            {
                error("Parse error: expected ')' at %s:%d\n",
                      pszInFile, iLine);
                return -1;
            }
        }

        /* Check for $if */
        else if (strncmp(pCurrentLine, "$if", 3) == 0)
        {
            int val;

            trace("found $if\n");
            /* Increase the if-level */
            iIfLevel++;

            /* Get beginning of the expression */
            p1 = GetNextChar(pCurrentLine + 3);

            /* evaluate the expression */
            val = EvaluateExpression(p1, 0);

            if (val)
            {
                iCopyLevel = iIfLevel;
            }
            else if (val == -1)
            {
                /* Parse error */
                return -1;
            }
        }

        /* Check for $include */
        else if (strncmp(pCurrentLine, "$include", 8) == 0)
        {
            int ret;

            trace("found $include\n");
            p1 = GetNextChar(pCurrentLine + 8);
            if (*p1 != '(')
            {
                error("Parse error: expected '(' at %s:%d, found '%c'\n",
                      pszInFile, iLine, *p1);
                return -1;
            }
            p1++;
            p2 = strchr(p1, ')');
            *p2 = 0;

            /* Parse the included file */
            ret = ParseInputFile(p1, fileOut);

            /* Restore the global file name */
            gpszCurFile = pszInFile;

            /* Restore the zeroed character */
            *p2 = ')';

            if (ret == -1)
            {
                return -1;
            }
        }

        /* Check for $$ comment */
        else if (strncmp(pCurrentLine, "$$", 2) == 0)
        {
            trace("$$ ignored\n");
            /* continue with next line */
            continue;
        }

        else
        {
            trace("wot:%s\n", pCurrentLine);
        }

        /* Continue with next line */
    }
    while (pCurrentLine = GetNextLine(pCurrentLine),
           iLine++,
           pCurrentLine != 0);

    /* Free the file data */
    free(pInputData);

    trace("Done with file.\n\n");

    return 0;
}


int
main(int argc, char* argv[])
{
    char *pszInFile, *pszOutFile;
    FILE* fileOut;
    int ret;

    if (argc != 3)
    {
        error("Usage: hpp <inputfile> <outputfile>\n");
        exit(1);
    }

    pszInFile = convert_path(argv[1]);
    pszOutFile = convert_path(argv[2]);

    fileOut = fopen(pszOutFile, "wb");
    if (fileOut == NULL)
    {
        error("Cannot open output file %s", pszOutFile);
        exit(1);
    }

    ret = ParseInputFile(pszInFile, fileOut);

    fclose(fileOut);
    free(pszInFile);
    free(pszOutFile);

    return ret;
}