/*
 * CiderPress
 * Copyright (C) 2007 by faddenSoft, LLC.  All Rights Reserved.
 * See the file LICENSE for distribution terms.
 */
/*
 * NiftyList database functions.
 *
 * The NList.Data file is divided into several sections, separated by lines
 * that start with a '*'.  They are:
 *
 *  ProDOS 8 MLI calls (prefaced with some comments)
 *  ProDOS 16 / GS/OS
 *  System tools (incl. GSBug goodies)
 *  User tools
 *  E1xxxx vectors
 *  E0xxxx vectors
 *  Softswitches and F8 ROM routines (plus Davex stuff at Bxxx)
 *  01xxxx vectors
 *  Nifty List service calls
 *  Resource type names
 *  Error codes (GS/OS and toolbox)
 *  HyperCardIIgs callbacks
 *  Request codes (for Finder extensions?)
 *
 * All lines have the general form:
 *  NNNN Text ...
 * where "NNNN" is a 4-digit hexadecimal value.
 *
 * All sections are in some sort of sort order, though it differs for toolbox
 * calls which are byte-reversed.  Generally speaking, do not expect the input
 * to be sorted already.  The original EOL char is 0x0d, but the file may be
 * converted for convenience in Windows, so supporting arbitrary EOLs is
 * useful.
 */
#include "StdAfx.h"
#include "Reformat.h"

/*static*/ NiftyList::DataSet NiftyList::fP8MLI = { 0 };
/*static*/ NiftyList::DataSet NiftyList::fGSOS = { 0 };
/*static*/ NiftyList::DataSet NiftyList::fSystemTools = { 0 };
/*static*/ NiftyList::DataSet NiftyList::fE1Vectors = { 0 };
/*static*/ NiftyList::DataSet NiftyList::fE0Vectors = { 0 };
/*static*/ NiftyList::DataSet NiftyList::f00Addrs = { 0 };
/*static*/ NiftyList::DataSet NiftyList::f01Vectors = { 0 };

/*static*/ char* NiftyList::fFileData = NULL;
/*static*/ bool NiftyList::fDataReady = false;

/*
 * Load the NiftyList data file.
 */
/*static*/ bool NiftyList::AppInit(const WCHAR* fileName)
{
    FILE* fp = NULL;
    long fileLen;
    char* pData;
    bool result = false;

    /*
     * Open the NList.Data file and load the contents into memory.
     */
    fp = _wfopen(fileName, L"rb");
    if (fp == NULL) {
        LOGI("NL Unable to open '%ls'", fileName);
        goto bail;
    } else {
        LOGI("NL Reading '%ls'", fileName);
    }

    if (fseek(fp, 0, SEEK_END) < 0) {
        LOGI("Seek failed");
        goto bail;
    }
    fileLen = ftell(fp);
    rewind(fp);

    fFileData = new char[fileLen];
    if (fFileData == NULL) {
        LOGI("Failed allocating %d bytes", fileLen);
        goto bail;
    }

    if (fread(fFileData, fileLen, 1, fp) != 1) {
        LOGI("Failed reading NList.Data (%d bytes)", fileLen);
        goto bail;
    }

    /*
     * Scan the data.
     */
    pData = fFileData;
    if (!ReadSection(&pData, &fileLen, &fP8MLI, kModeNormal))
        goto bail;
    if (!ReadSection(&pData, &fileLen, &fGSOS, kModeNormal))
        goto bail;
    if (!ReadSection(&pData, &fileLen, &fSystemTools, kModeNormal))
        goto bail;
    if (!ReadSection(&pData, &fileLen, NULL, kModeSkip)) // user tools
        goto bail;
    if (!ReadSection(&pData, &fileLen, &fE1Vectors, kModeNormal))
        goto bail;
    if (!ReadSection(&pData, &fileLen, &fE0Vectors, kModeNormal))
        goto bail;
    if (!ReadSection(&pData, &fileLen, &f00Addrs, kModeNormal))
        goto bail;
    if (!ReadSection(&pData, &fileLen, &f01Vectors, kModeNormal))
        goto bail;

    //DumpSection(fP8MLI);
    //DumpSection(fGSOS);

    fDataReady = true;
    result = true;
    LOGI("NL NiftyList data loaded");

bail:
    if (fp != NULL)
        fclose(fp);
    return result;
}

/*
 * Discard all allocated storage.
 */
/*static*/ bool NiftyList::AppCleanup(void)
{
    delete[] fP8MLI.pEntries;
    delete[] fGSOS.pEntries;
    delete[] fSystemTools.pEntries;
    delete[] fE1Vectors.pEntries;
    delete[] fE0Vectors.pEntries;
    delete[] f00Addrs.pEntries;
    delete[] f01Vectors.pEntries;

    delete[] fFileData;

    fDataReady = false;
    return true;
}


/*
 * Slurp one section from the data file.  Stomps EOL markers.
 *
 * Leaves "*ppData" pointing at the start of the next section.  "*pRemLen"
 * is updated appropriately.
 */
/*static*/ bool NiftyList::ReadSection(char** ppData, long* pRemLen,
    DataSet* pSet, LoadMode mode)
{
    assert(ppData != NULL);
    assert(pRemLen != NULL);
    assert(*ppData != NULL);
    assert(*pRemLen > 0);
    assert(pSet != NULL || mode == kModeSkip);
    assert(mode == kModeNormal || mode == kModeSkip);

    char* pData = *ppData;
    long remLen = *pRemLen;
    int lineLen, numLines, entry;

    /*
     * Count up the #of entries in this section.
     */
    numLines = 0;
    while (1) {
        lineLen = ScanLine(pData, remLen);
        if (lineLen == 0) {
            LOGI("Failed scanning line, remLen=%ld", remLen);
            return false;
        }
        if (*pData == '*') {
            pData += lineLen;
            remLen -= lineLen;
            break;      // end of section reached
        }

        pData += lineLen;
        remLen -= lineLen;
        numLines++;
    }

    if (mode == kModeSkip) {
        LOGI(" NL Skipping %d entries in section", numLines);
        *ppData = pData;
        *pRemLen = remLen;
        return true;
    } else {
        LOGI(" NL Found %d entries in section", numLines);
    }

    /*
     * Allocate storage for the lookup array.
     */
    assert(pSet->numEntries == 0 && pSet->pEntries == NULL);

    pSet->pEntries = new NameValue[numLines];
    if (pSet->pEntries == NULL) {
        LOGI("NameValue alloc failed");
        return false;
    }
    pSet->numEntries = numLines;

    /*
     * Add the entries to the list.
     */
    pData = *ppData;
    remLen = *pRemLen;
    entry = 0;
    while (1) {
        lineLen = ScanLine(pData, remLen);
        if (lineLen == 0) {
            LOGI("Failed scanning line(2), remLen=%ld", remLen);
            return false;
        }

        if (*pData == '*') {
            pData += lineLen;
            remLen -= lineLen;
            break;      // end of section reached
        }

        if (lineLen < 6 || pData[4] != ' ') {
            LOGI("Found garbled line '%.80hs'", pData);
            return false;
        }
        if (pData[lineLen-2] == '\r' || pData[lineLen-2] == '\n')
            pData[lineLen-2] = '\0';
        else if (pData[lineLen-1] == '\r' || pData[lineLen-1] == '\n')
            pData[lineLen-1] = '\0';
        else {
            LOGI("No EOL found on '%.80hs' (%d)", pData, lineLen);
        }

        assert(entry < numLines);
        pSet->pEntries[entry].name = pData +5;
        pSet->pEntries[entry].value = ConvHexFour(pData);
        entry++;

        pData += lineLen;
        remLen -= lineLen;
    }
    assert(entry == numLines);

    *ppData = pData;
    *pRemLen = remLen;

    /*
     * Sort the array.  In most cases it will already be in sorted order,
     * which is a worst-case for qsort, but the only really big section
     * (toolbox calls) is byte-swapped and sorts just fine with qsort.
     */
    qsort(pSet->pEntries, numLines, sizeof(NameValue), SortNameValue);

    return true;
}

/*
 * Scan a line of text.  Return the #of chars, including the EOL bytes.
 *
 * Returns 0 if there's no data, which can only happen if "remLen" is zero
 * (i.e. we hit EOF).
 */
/*static*/ int NiftyList::ScanLine(const char* pData, long remLen)
{
    bool atEOL = false;
    int lineLen = 0;

    while (remLen--) {
        if (*pData == '\r' || *pData == '\n')
            atEOL = true;
        else if (atEOL)
            break;
        pData++;
        lineLen++;
    }

    return lineLen;
}

/*
 * qsort() sort function.
 */
/*static*/ int NiftyList::SortNameValue(const void* v1, const void* v2)
{
    const NameValue* pnv1 = (const NameValue*) v1;
    const NameValue* pnv2 = (const NameValue*) v2;

    if (pnv1->value > pnv2->value)
        return 1;
    else if (pnv1->value < pnv2->value)
        return -1;
    else {
        DebugBreak();   // should never be equal in well-formed file
        return 0;
    }
}

/*
 * Hex digit converter.
 */
static inline int HexValue(char ch)
{
    if (ch >= '0' && ch <= '9')
        return ch - '0';
    if (ch >= 'A' && ch <= 'F')
        return ch - 'A' +10;
    if (ch >= 'a' && ch <= 'f')
        return ch - 'a' +10;
    DebugBreak();       // shouldn't happen on well-formed file
    return -1;
}

/*
 * Convert a 4-char hexadecimal value to an unsigned 16-bit value.
 */
/*static*/ uint16_t NiftyList::ConvHexFour(const char* data)
{
    return HexValue(data[0]) << 12 |
          HexValue(data[1]) << 8 |
          HexValue(data[2]) << 4 |
          HexValue(data[3]);
}

/*
 * Dump the contents of a section.
 */
/*static*/ void NiftyList::DumpSection(const DataSet& dataSet)
{
    long ent;

    LOGD("Dumping section (count=%ld)", dataSet.numEntries);

    for (ent = 0; ent < dataSet.numEntries; ent++) {
        LOGD("%4d: %04x '%hs'",
            ent, dataSet.pEntries[ent].value, dataSet.pEntries[ent].name);
    }
}

/*
 * Look up a value in the specified table.  Returns the name, or NULL if
 * not found.
 *
 * This uses a binary search, so the entries must be in sorted order.
 */
/*static*/ const char* NiftyList::Lookup(const DataSet& dataSet, uint16_t key)
{
    if (!fDataReady) {
        return NULL;
    }
    assert(dataSet.numEntries > 0);

    int high = dataSet.numEntries-1;
    int low = 0;
    int mid;

    while (low <= high) {
        mid = (high + low)/2;
        uint16_t midVal = dataSet.pEntries[mid].value;

        if (key > midVal)
            low = mid +1;
        else if (key < midVal)
            high = mid -1;
        else
            return dataSet.pEntries[mid].name;
    }

    return NULL;
}
