#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdio.h>
#include <assert.h>
#include <string.h>
#include <pslib.h>

#include "pmFPA.h"
#include "pmHDU.h"
#include "pmHDUUtils.h"
#include "pmConcepts.h"
#include "pmConceptsRead.h"

//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// File-static functions
//////////////////////////////////////////////////////////////////////////////////////////////////////////////

static bool compareConcepts(const psMetadataItem *compare, // Item to compare
                            const psMetadataItem *standard // Standard for comparison
                           )
{
    // First order checks
    if (! compare || ! standard) {
        return false;
    }
    if (strcasecmp(compare->name, standard->name) != 0) {
        return false;
    }

    // Special case: list
    if (compare->type == PS_DATA_LIST) {
        // "compare" contains a list of psMetadataItems
        // "standard" likely contains just a string (but it might possibly be a list of strings)
        psList *cList = compare->data.V; // The list from comparison item
        psList *sList = NULL;         // The list from standard item
        switch (standard->type) {
        case PS_DATA_STRING:
            sList = psStringSplit(standard->data.V, " ;", true);
            break;
        case PS_DATA_LIST:
            sList = psMemIncrRefCounter(standard->data.V);
            break;
        default:
            return false;
        }
        if (cList->n != sList->n) {
            psFree(sList);
            return false;
        }
        psVector *match = psVectorAlloc(cList->n, PS_TYPE_U8); // Array indicating which values match
        psVectorInit(match, 0);
        psListIterator *cIter = psListIteratorAlloc(cList, PS_LIST_HEAD, false); // compare iterator
        psListIterator *sIter = psListIteratorAlloc(sList, PS_LIST_HEAD, false); // standard iterator
        psMetadataItem *cItem = NULL; // Item from compare list
        while ((cItem = psListGetAndIncrement(cIter))) {
            if (cItem->type != PS_DATA_STRING) {
                psLogMsg(__func__, PS_LOG_WARN, "psMetadataItem from list is of type %x instead of "
                         "%x (PS_DATA_STRING) --- can't interpret.\n", cItem->type, PS_DATA_STRING);
                psFree(cIter);
                psFree(sIter);
                psFree(match);
                psFree(sList);
                return false;
            }
            psString cString = cItem->data.V; // String from compare list
            psListIteratorSet(sIter, PS_LIST_HEAD);
            int index = 0;            // Index for list
            bool found = false;       // Found a match?
            for (psString sString = NULL; (sString = psListGetAndIncrement(sIter)) && !found; index++) {
                if (strcasecmp(cString, sString) == 0) {
                    match->data.U8[index]++;
                    found = true;
                }
            }
            if (! found) {
                // Can give up immediately
                psFree(cIter);
                psFree(sIter);
                psFree(match);
                psFree(sList);
                return false;
            }
        }
        // Make sure we got 100% matches in both directions
        bool allMatch = true;         // Did all of them match?
        for (int i = 0; i < match->n && allMatch; i++) {
            if (!match->data.U8[i]) {
                allMatch = false;
            }
        }
        psFree(cIter);
        psFree(sIter);
        psFree(sList);
        psFree(match);
        return allMatch;
    }

    return psMetadataItemCompare(compare, standard);

}


// Format a single concept
static psMetadataItem *conceptFormat(const pmConceptSpec *spec, // The concept specification
                                     const psMetadataItem *concept, // The concept to parse
                                     const psMetadata *cameraFormat, // The camera format
                                     const pmFPA *fpa, // The FPA
                                     const pmChip *chip, // The chip
                                     const pmCell *cell // The cell
                                    )
{
    assert(spec);
    assert(cameraFormat);

    if (concept) {
        psMetadataItem *formatted = NULL;  // The formatted concept
        if (spec->format) {
            formatted = spec->format(concept, cameraFormat, fpa, chip, cell);
        } else if (strcmp(concept->name, spec->blank->name) != 0) {
            // Adjust so that the name is correct
            formatted = psMetadataItemCopy(concept);
            psFree(formatted->name);
            formatted->name = psStringCopy(spec->blank->name);
        } else {
            // Can get away with merely incrementing the reference counter
            formatted = psMemIncrRefCounter((const psPtr)concept);
        }

        return formatted;
    }
    return NULL;
}

// Write a single value to a header
static bool writeSingleHeader(pmHDU *hdu, // HDU for which to add to the header
                              const char *keyword, // Keyword to add
                              const psMetadataItem *item // Item to add to the header; may be NULL
                             )
{
    assert(hdu);
    assert(keyword && strlen(keyword) > 0);

    if (!hdu->header) {
        hdu->header = psMetadataAlloc();
    }
    if (!item) {
        psTrace("psModules.concepts", 9, "Writing header %s: <<<BLANK>>>\n", keyword);
        // Assume it's a NULL string: it's most easily parsed.
        return psMetadataAddStr(hdu->header, PS_LIST_TAIL, keyword, PS_META_REPLACE, NULL, NULL);
    }
    switch (item->type) {
    case PS_DATA_STRING:
        psTrace("psModules.concepts", 9, "Writing header %s: %s\n", keyword, item->data.str);
        return psMetadataAddStr(hdu->header, PS_LIST_TAIL, keyword, PS_META_REPLACE, item->comment,
                                item->data.V);
    case PS_DATA_S32:
        psTrace("psModules.concepts", 9, "Writing header %s: %d\n", keyword, item->data.S32);
        return psMetadataAddS32(hdu->header, PS_LIST_TAIL, keyword, PS_META_REPLACE, item->comment,
                                item->data.S32);
    case PS_DATA_F32:
        psTrace("psModules.concepts", 9, "Writing header %s: %f\n", keyword, item->data.F32);
        return psMetadataAddF32(hdu->header, PS_LIST_TAIL, keyword, PS_META_REPLACE, item->comment,
                                item->data.F32);
    case PS_DATA_F64:
        psTrace("psModules.concepts", 9, "Writing header %s: %f\n", keyword, item->data.F64);
        return psMetadataAddF64(hdu->header, PS_LIST_TAIL, keyword, PS_META_REPLACE, item->comment,
                                item->data.F64);
    case PS_DATA_REGION: {
            psString region = psRegionToString(*(psRegion*)item->data.V);
            psTrace("psModules.concepts", 9, "Writing header %s: %s\n", keyword, region);
            bool result = psMetadataAddStr(hdu->header, PS_LIST_TAIL, keyword, PS_META_REPLACE, item->comment,
                                           region);
            psFree(region);
            return result;
        }
    default:
        psLogMsg(__func__, PS_LOG_WARN, "Type of %s is not suitable for a FITS header --- not added.\n",
                 item->name);
        return false;
    }
}


// Write potentially multiple values to a header
static bool writeHeader(pmHDU *hdu,     // HDU for which to add to the header
                        const char *keywords, // Keywords to add
                        const psMetadataItem *item // Item to add to the header
                       )
{
    assert(hdu);
    assert(keywords);
    assert(item);

    bool status = true;                 // Status of writing headers, to be returned
    if (item->type == PS_DATA_LIST) {
        psList *values = item->data.V;  // List of outputs
        psList *keys = psStringSplit(keywords, " ,;", true); // List of keywords
        if (keys->n != values->n && values->n != 0) {
            psError(PS_ERR_UNKNOWN, true, "Number of keywords (%ld) does not match number of "
                    "values (%ld).\n", keys->n, values->n);
            psFree(keys);
            return false;
        }
        psListIterator *keysIter = psListIteratorAlloc(keys, PS_LIST_HEAD, false); // Iterator for keywords
        psListIterator *valuesIter = psListIteratorAlloc(values, PS_LIST_HEAD, false); // Iterator for values
        psString key = NULL;            // Keyword from iteration
        while ((key = psListGetAndIncrement(keysIter))) {
            psMetadataItem *value = psListGetAndIncrement(valuesIter); // Value from iteration; may be NULL
            status &= writeSingleHeader(hdu, key, value);
        }
        psFree(keysIter);
        psFree(valuesIter);
        psFree(keys);
    } else {
        status = writeSingleHeader(hdu, keywords, item);
    }
    return status;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Public functions
//////////////////////////////////////////////////////////////////////////////////////////////////////////////

bool p_pmConceptsWriteToCells(const psMetadata *specs, const pmCell *cell, const psMetadata *concepts)
{
    PS_ASSERT_PTR_NON_NULL(specs, false);
    PS_ASSERT_PTR_NON_NULL(concepts, false);
    if (!cell) {
        return false;
    }
    if (!cell->config) {
        return false;
    }

    pmHDU *hdu = pmHDUGetLowest(NULL, NULL, cell); // The HDU at the lowest level
    if (!hdu) {
        return false;
    }
    psMetadata *cameraFormat = hdu->format; // The camera format
    psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
    psMetadataItem *specItem = NULL;    // Item from the specs metadata
    while ((specItem = psMetadataGetAndIncrement(specsIter))) {
        pmConceptSpec *spec = specItem->data.V; // The specification
        psString name = specItem->name; // The concept name
        psMetadataItem *cameraItem = psMetadataLookup(cell->config, name); // The concept from the camera,
        // or NULL
        if (cameraItem) {
            // Grab the concept
            psMetadataItem *conceptItem = psMetadataLookup(concepts, name); // The concept
            // Formatted version
            psMetadataItem *formatted = conceptFormat(spec, conceptItem, cameraFormat, NULL, NULL, cell);
            if (!formatted) {
                continue;
            }
            psString nameSource = NULL; // String with the concept name and ".SOURCE" added
            psStringAppend(&nameSource, "%s.SOURCE", name);
            bool mdok = true;       // Status of MD lookup
            psString source = psMetadataLookupStr(&mdok, cell->config, nameSource); // The source
            if (mdok && strlen(source) > 0) {
                psTrace("psModules.concepts", 8, "%s is %s\n", nameSource, source);
                if (strcasecmp(source, "HEADER") == 0) {
                    if (cameraItem->type != PS_DATA_STRING) {
                        psLogMsg(__func__, PS_LOG_WARN, "Concept %s is specified by header, but is not "
                                 "of type STR --- ignored.\n", conceptItem->name);
                        continue;
                    }
                    psTrace("psModules.concepts", 8, "Writing %s to header %s\n", name, cameraItem->data.str);
                    writeHeader(hdu, cameraItem->data.V, formatted);
                } else if (strcasecmp(source, "VALUE") == 0) {
                    psTrace("psModules.concepts", 8, "Checking %s against camera format.\n", name);
                    if (! compareConcepts(formatted, cameraItem)) {
                        psLogMsg(__func__, PS_LOG_WARN, "Concept %s is specified by value in the camera "
                                 "format, but the values don't match.\n", name);
                    }
                } else {
                    psLogMsg(__func__, PS_LOG_WARN, "Concept source %s isn't HEADER or VALUE --- can't "
                             "write\n", nameSource);
                }
            } else if (! compareConcepts(formatted, cameraItem)) {
                // Assume it's specified by value
                psLogMsg(__func__, PS_LOG_WARN, "Concept %s is specified by value in the camera "
                         "format, but the values don't match.\n", name);
            }
            psFree(formatted);
            psFree(nameSource);
        }

    }
    psFree(specsIter);
    return true;
}

bool p_pmConceptsWriteToDefaults(const psMetadata *specs, const pmFPA *fpa, const pmChip *chip,
                                 const pmCell *cell, psMetadata *concepts)
{
    PS_ASSERT_PTR_NON_NULL(specs, false);
    PS_ASSERT_PTR_NON_NULL(concepts, false);

    pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // The HDU at the lowest level
    if (!hdu) {
        return false;
    }
    psMetadata *cameraFormat = hdu->format; // The camera format
    bool mdok = true;                   // Status of MD lookup
    psMetadata *defaults = psMetadataLookupMetadata(&mdok, cameraFormat, "DEFAULTS"); // The DEFAULTS spec
    if (!mdok || !defaults) {
        return false;
    }

    psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
    psMetadataItem *specItem = NULL;    // Item from the specs metadata
    while ((specItem = psMetadataGetAndIncrement(specsIter))) {
        pmConceptSpec *spec = specItem->data.V; // The specification
        psString name = specItem->name; // The concept name
        psMetadataItem *defaultItem = psMetadataLookup(defaults, name); // The item from the DEFAULTS
        if (defaultItem) {
            psMetadataItem *conceptItem = NULL; // The item from the concepts
            if (defaultItem->type == PS_DATA_METADATA) {
                // It's a menu --- need to look up the .DEPEND
                psString dependName = NULL; // The concept name with ".DEPEND" on the end
                psStringAppend(&dependName, "%s.DEPEND", defaultItem->name);
                psString dependKey = psMetadataLookupStr(&mdok, defaults, dependName); // The keyword
                if (!mdok || !dependKey || strlen(dependKey) == 0) {
                    psLogMsg(__func__, PS_LOG_WARN, "Can't find %s in the DEFAULTS for %s --- ignored.\n",
                             dependName, name);
                    psFree(dependName);
                    continue;
                }
                psString dependValue = psMetadataLookupStr(&mdok, concepts, dependKey); // The value
                if (!mdok || !dependValue || strlen(dependValue) == 0) {
                    psLogMsg(__func__, PS_LOG_WARN, "Concept %s specified by %s isn't of type STR -- "
                             "ignored.\n", dependKey, dependName);
                    psFree(dependName);
                    continue;
                }
                psFree(dependName);
                // Get the actual item of interest, and correct the name to match the concept name
                defaultItem = psMetadataItemCopy(psMetadataLookup(defaultItem->data.md, dependValue));
                psFree(defaultItem->name);
                defaultItem->name = psStringCopy(name);
            } else {
                psMemIncrRefCounter(defaultItem);
            }
            conceptItem = psMetadataLookup(concepts, name); // The item from the concepts
            psMetadataItem *formatted = conceptFormat(spec, conceptItem, cameraFormat, fpa, chip, cell);
            if (!formatted) {
                continue;
            }
            if (! compareConcepts(formatted, defaultItem)) {
                psLogMsg(__func__, PS_LOG_WARN, "Concept %s is specified by the DEFAULTS in the camera "
                         "format, but the values don't match.\n", name);
            }
            psFree(defaultItem);
            psFree(formatted);
        }
    }
    psFree(specsIter);
    return true;
}


bool p_pmConceptsWriteToHeader(const psMetadata *specs, const pmFPA *fpa, const pmChip *chip,
                               const pmCell *cell, const psMetadata *concepts)
{
    PS_ASSERT_PTR_NON_NULL(specs, false);
    PS_ASSERT_PTR_NON_NULL(concepts, false);

    pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // The HDU at the lowest level
    if (!hdu) {
        return false;
    }
    psMetadata *cameraFormat = hdu->format; // The camera format
    bool mdok = true;                   // Status of MD lookup
    psMetadata *translation = psMetadataLookupMetadata(&mdok, cameraFormat, "TRANSLATION"); // The TRANSLATION spec
    if (!mdok || !translation) {
        return false;
    }

    psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
    psMetadataItem *specItem = NULL;    // Item from the specs metadata
    while ((specItem = psMetadataGetAndIncrement(specsIter))) {
        pmConceptSpec *spec = specItem->data.V; // The specification
        psString name = specItem->name; // The concept name
        psMetadataItem *headerItem = psMetadataLookup(translation, name); // The item from the TRANSLATION
        if (headerItem) {
            if (headerItem->type != PS_DATA_STRING) {
                psLogMsg(__func__, PS_LOG_WARN, "TRANSLATION keyword for concept %s isn't of type STR ---"
                         " ignored.", name);
                continue;
            }
            psTrace("psModules.concepts", 3, "Writing %s to header %s\n", name, headerItem->data.str);
            psMetadataItem *conceptItem = psMetadataLookup(concepts, name); // The item from the concepts
            psMetadataItem *formatted = conceptFormat(spec, conceptItem, cameraFormat, fpa, chip, cell);
            if (!formatted) {
                continue;
            }
            writeHeader(hdu, headerItem->data.V, formatted);
            psFree(formatted);
        }
    }
    psFree(specsIter);
    return true;
}

// XXX Warning: This code has not been tested at all
bool p_pmConceptsWriteToDatabase(const psMetadata *specs, const pmFPA *fpa, const pmChip *chip,
                                 const pmCell *cell, psDB *db, const psMetadata *concepts)
{
    PS_ASSERT_PTR_NON_NULL(specs, false);
    PS_ASSERT_PTR_NON_NULL(concepts, false);

    if (!db) {
        return false;
    }

    #ifndef HAVE_PSDB
    return false;
    #else

    pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // The HDU at the lowest level
    if (!hdu) {
        return false;
    }
    psMetadata *cameraFormat = hdu->format; // The camera format
    bool mdok = true;                   // Status of MD lookup
    psMetadata *database = psMetadataLookupMetadata(&mdok, cameraFormat, "DATABASE"); // The DATABASE spec
    if (!mdok || !database) {
        return false;
    }
    psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
    psMetadataItem *specItem = NULL;    // Item from the specs metadata
    while ((specItem = psMetadataGetAndIncrement(specsIter))) {
        pmConceptSpec *spec = specItem->data.V; // The specification
        psString name = specItem->name; // The concept name

        psMetadataItem *dbItem = psMetadataLookup(database, name); // The item from the DATABASE
        if (dbItem) {
            if (dbItem->type != PS_DATA_METADATA) {
                psLogMsg(__func__, PS_LOG_WARN, "DATABASE keyword for concept %s isn't of type METADATA "
                         "--- ignored.\n", name);
                continue;
            }

            psMetadataItem *conceptItem = psMetadataLookup(concepts, name); // The item from the concepts
            psMetadataItem *formatted = conceptFormat(spec, conceptItem, cameraFormat, fpa, chip, cell);
            if (!formatted) {
                continue;
            }

            psMetadata *dbLookup = dbItem->data.V; // How to look up the value of interest
            // Name of the table
            const char *tableName = psMetadataLookupStr(&mdok, dbLookup, "TABLE");
            // Name of "where" columns
            const char *givenCols = psMetadataLookupStr(&mdok, dbLookup, "GIVENDBCOL");
            // Values for "where" columns
            const char *givenPS = psMetadataLookupStr(&mdok, dbLookup, "GIVENPS");

            // Now, need to get the "given"s
            if (strlen(givenCols) || strlen(givenPS)) {
                psList *cols = psStringSplit(givenCols, ",;", true); // List of column names
                psList *values = psStringSplit(givenPS, ",;", true); // List of value names for the columns
                psMetadata *selection = psMetadataAlloc(); // The stuff to select in the DB
                if (cols->n != values->n) {
                    psLogMsg(__func__, PS_LOG_WARN,
                             "The GIVENDBCOL and GIVENPS entries for %s do not have "
                             "the same number of entries --- ignored.\n", name);
                } else {
                    // Iterators for the lists
                    psListIterator *colsIter = psListIteratorAlloc(cols, PS_LIST_HEAD, false);
                    psListIterator *valuesIter = psListIteratorAlloc(values, PS_LIST_HEAD, false);
                    char *column = NULL;    // Name of the column
                    while ((column = psListGetAndIncrement(colsIter))) {
                        char *dependName = psListGetAndIncrement(valuesIter); // Name for the value
                        if (!strlen(column) || !strlen(name)) {
                            psLogMsg(__func__, PS_LOG_WARN, "One of the columns or value names for %s is "
                                     " empty --- ignored.\n", name);
                        } else {
                            // Search for the value name
                            psMetadataItem *item = NULL; // The value
                            if (!item && cell) {
                                item = psMetadataLookup(cell->concepts, dependName);
                            }
                            if (!item && chip) {
                                item = psMetadataLookup(chip->concepts, dependName);
                            }
                            if (!item && fpa) {
                                item = psMetadataLookup(fpa->concepts, dependName);
                            }
                            if (! item) {
                                psLogMsg(__func__, PS_LOG_ERROR,
                                         "Unable to find the value name %s for DB "
                                         " lookup on %s --- ignored.\n", dependName, name);
                            } else {
                                // We need to create a new psMetadataItem.  I don't think we can't simply
                                // hack the existing one, since that could conceivably cause memory leaks
                                psMetadataAddItem(selection, formatted, PS_LIST_TAIL, PS_META_REPLACE);
                                psFree(formatted);
                            }
                        }
                        psFree(dependName);
                        psFree(column);
                    } // Iterating through the columns
                    psFree(colsIter);
                    psFree(valuesIter);

                    // Check first to make sure we're only going to touch one row
                    psArray *dbResult = psDBSelectRows(db, tableName, selection, 2); // Lookup result
                    // Note that we use limit=2 in order to test if there are multiple rows returned
                    if (! dbResult || dbResult->n == 0) {
                        psLogMsg(__func__, PS_LOG_WARN, "Unable to find any rows in DB for %s --- "
                                 "ignored\n", name);
                        return false;
                    } else {
                        if (dbResult->n > 1) {
                            psLogMsg(__func__, PS_LOG_WARN, "Multiple rows returned in DB lookup for %s "
                                     "--- ignored.\n", name);
                        }
                        // Update the DB
                        psMetadata *update = psMetadataAlloc();
                        psMetadataAddItem(update, conceptItem, PS_LIST_HEAD, 0);
                        psDBUpdateRows(db, tableName, selection, update);
                        psFree(update);
                        return true;
                    }
                }
                psFree(cols);
                psFree(values);
            } // Doing the "given"s.
        }
    }
    psFree(specsIter);
    return true;
    #endif
}

