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

#include <stdio.h>
#include <assert.h>
#include <string.h>
#include <strings.h>		/* for strn?casecmp */
#include <pslib.h>

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

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

// This function gets called for the really boring concepts --- where all you have to do is parse from a
// header or database and you don't need to muck around with conversions.  There is no similar "formatPlain",
// since the type is already known.
static psMetadataItem *parsePlain(psMetadataItem *concept, // The concept to parse
                                  const psMetadataItem *pattern // The concept pattern
                                 )
{
    assert(concept);
    assert(pattern);

    switch (pattern->type) {
    case PS_DATA_STRING: {
            psString string = psMetadataItemParseString(concept); // Get the string, so I can free it after it
            // goes on the MetadataItem
            psMetadataItem *item = psMetadataItemAllocStr(pattern->name, pattern->comment, string);
            psFree(string);
            return item;
        }
    case PS_DATA_S32:
        return psMetadataItemAllocS32(pattern->name, pattern->comment, psMetadataItemParseS32(concept));
    case PS_DATA_F32:
        return psMetadataItemAllocF32(pattern->name, pattern->comment, psMetadataItemParseF32(concept));
    case PS_DATA_F64:
        return psMetadataItemAllocF64(pattern->name, pattern->comment, psMetadataItemParseF64(concept));
    default:
        psLogMsg(__func__, PS_LOG_WARN, "Concept %s (%s) is not of a standard type (%x)\n",
                 pattern->name, pattern->comment, pattern->type);
        return NULL;
    }
}

// Parse a single concept
static bool conceptParse(pmConceptSpec *spec, // The concept specification
                         psMetadataItem *concept, // The concept to parse
                         pmConceptSource source, // The concept source
                         psMetadata *cameraFormat, // The camera format
                         psMetadata *target, // The target
                         const pmFPA *fpa, // The FPA
                         const pmChip *chip, // The chip
                         const pmCell *cell // The cell
                        )
{
    assert(spec);
    assert(cameraFormat);
    assert(target);

    if (!concept) {
        psError(PS_ERR_UNKNOWN, true, "Concept is NULL");
        return false;
    }

    psMetadataItem *parsed = NULL;  // The parsed concept
    if (spec->parse) {
        parsed = spec->parse(concept, spec->blank, source, cameraFormat, fpa, chip, cell);
    } else {
        parsed = parsePlain(concept, spec->blank);
    }
    if (!parsed) {
        if (spec->required) {
            psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to parse concept %s\n", spec->blank->name);
            return false;
        } else {
            psWarning("Unable to parse concept %s, but concept not marked as required.\n", spec->blank->name);
            psErrorClear();
            return true; // XXX return?
        }
    }

    psTrace ("psModules.concepts", 3, "parsing concept: %s\n", spec->blank->name);

    // Plug the parsed concept into a new psMetadataItem, so each "concept" has its own version that can
    // be altered without affecting the others.  Also, so that we maintain the template name and comment.
    psMetadataItem *cleaned = NULL;     // Item that's been cleaned up --- correct name and comment
    switch (spec->blank->type) {
    case PS_DATA_STRING:
        cleaned = psMetadataItemAllocStr(spec->blank->name, spec->blank->comment, parsed->data.V);
        break;
    case PS_DATA_S32:
        cleaned = psMetadataItemAllocS32(spec->blank->name, spec->blank->comment, parsed->data.S32);
        break;
    case PS_DATA_F32:
        cleaned = psMetadataItemAllocF32(spec->blank->name, spec->blank->comment, parsed->data.F32);
        break;
    case PS_DATA_F64:
        cleaned = psMetadataItemAllocF64(spec->blank->name, spec->blank->comment, parsed->data.F64);
        break;
    default:
        cleaned = psMetadataItemAlloc(spec->blank->name, parsed->type, spec->blank->comment,
                                      parsed->data.V);
    }
    psFree(parsed);
    psMetadataAddItem(target, cleaned, PS_LIST_TAIL, PS_META_REPLACE);
    psFree(cleaned);                 // Drop reference
    return true;
}


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

bool p_pmConceptsReadFromCells(psMetadata *target, const psMetadata *specs, const pmCell *cell)
{
    PS_ASSERT_PTR_NON_NULL(specs, false);
    PS_ASSERT_PTR_NON_NULL(target, false);
    if (!cell) {
        psError(PS_ERR_UNKNOWN, true, "cell is NULL");
        return false;
    }

    pmHDU *hdu = pmHDUGetLowest(NULL, NULL, cell); // The HDU at the lowest level
    if (!hdu) {
        psError(PS_ERR_UNKNOWN, true, "Can't find HDU for cell");
        return false;
    }
    psMetadata *cameraFormat = hdu->format; // The camera format
    psMetadata *cellConfig = cell->config; // The camera configuration for this cell
    psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
    psMetadataItem *specItem = NULL;    // Item from the specs metadata
    bool status = true;                 // Status of reading concepts
    while ((specItem = psMetadataGetAndIncrement(specsIter))) {
        pmConceptSpec *spec = specItem->data.V; // The specification
        psString name = specItem->name; // The concept name
        psMetadataItem *conceptItem = psMetadataLookup(cellConfig, name); // The concept, or NULL
        psTrace("psModules.concepts", 10, "%s: %p\n", name, conceptItem);
        if (conceptItem) {
            if (conceptItem->type == PS_DATA_STRING) {
                // Check the SOURCE
                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
                psFree(nameSource);
                if (mdok && source && strlen(source) > 0 && strcasecmp(source, "VALUE") == 0) {
                    if (!conceptParse(spec, conceptItem, PM_CONCEPT_SOURCE_CELLS,
                                      cameraFormat, target, NULL, NULL, cell)) {
                        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to parse concept %s from camera "
                                "configuration\n", name);
                        status = false;
                    }
                } else if (source && (strlen(source) == 0 || strcasecmp(source, "HEADER") != 0)) {
                    // We leave "HEADER" to pmConceptsReadFromHeader
                    psError(PS_ERR_IO, true, "%s isn't HEADER or VALUE --- can't read %s\n", source,
                            name);
                    continue;
                }
            } else {
                // Another type --- should be OK
                if (!conceptParse(spec, conceptItem, PM_CONCEPT_SOURCE_CELLS,
                                  cameraFormat, target, NULL, NULL, cell)) {
                    psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to parse concept %s from camera "
                            "configuration.\n", name);
                    status = false;
                }
            }
        }
    }
    psFree(specsIter);
    return status;
}


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

    psTrace("psModules.concepts", 3, "Reading concepts from defaults...\n");

    pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // The HDU at the lowest level
    if (!hdu) {
        // We read the defaults for all the HDUs we could find
        return true;
    }
    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) {
        psError(PS_ERR_IO, true, "Failed to find \"DEFAULTS\"");
        return false;
    }
    psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
    psMetadataItem *specItem = NULL;    // Item from the specs metadata
    bool status = true;                 // Status of reading concepts
    psErrorClear();   // we're going to declare all errors "old" => won't clear stack
    while ((specItem = psMetadataGetAndIncrement(specsIter))) {
        pmConceptSpec *spec = specItem->data.V; // The specification
        psString name = specItem->name; // The concept name
        psMetadataItem *conceptItem = psMetadataLookup(defaults, name); // The concept, or NULL
        psTrace("psModules.concepts", 10, "%s: %p\n", name, conceptItem);
        if (conceptItem && conceptItem->type == PS_DATA_METADATA) {
            psTrace("psModules.concepts", 5, "%s is of type METADATA.\n", name);
            // Check for DEPEND
            psMetadata *dependMenu = conceptItem->data.V; // The DEPEND menu
            psString depend = NULL; // The CONCEPT.DEPEND
            psStringAppend(&depend, "%s.DEPEND", name);
            const char *dependConcept = psMetadataLookupStr(&mdok, defaults, depend); // The concept name
            if (!mdok || !dependConcept || strlen(dependConcept) == 0) {
                psError(PS_ERR_IO, true, "Unable to parse %s: couldn't find %s in DEFAULTS.\n", name,
                        depend);
                psFree(depend);
                continue;
            }
            psFree(depend);
            // Now look up the depend value
            psMetadataItem *dependValue = NULL; // The value of the concept we're looking up
            if (cell) {
                dependValue = psMetadataLookup(cell->concepts, dependConcept);
            }
            if (chip && !dependValue) {
                dependValue = psMetadataLookup(chip->concepts, dependConcept);
            }
            if (fpa && !dependValue) {
                dependValue = psMetadataLookup(chip->concepts, dependConcept);
            }
            if (!dependValue) {
                psError(PS_ERR_IO, true, "Unable to find %s to resolve %s in DEFAULTS.\n",
                        dependConcept, name);
                continue;
            }
            if (dependValue->type != PS_DATA_STRING) {
                psError(PS_ERR_BAD_PARAMETER_TYPE, true, "%s is required to resolve %s in DEFAULTS, "
                        "but it is not of type STRING.\n", dependConcept, name);
                continue;
            }
            const char *dependKey = dependValue->data.V; // The key to the DEPEND menu
            psTrace("psModules.concepts", 7, "%s.DEPEND resolves to %s....\n", name, dependKey);

            conceptItem = psMetadataLookup(dependMenu, dependKey);
            // Now we can parse this as we would ordinarily
        }
        if (conceptItem && !conceptParse(spec, conceptItem, PM_CONCEPT_SOURCE_DEFAULTS,
                                         cameraFormat, target, fpa, chip, cell)) {
            psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to parse concept %s from DEFAULTS.\n", name);
            status = false;
        }
    }
    psFree(specsIter);
    return status;
}


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

    pmHDU *hduLow = pmHDUGetLowest(fpa, chip, cell); // The HDU at the lowest level
    if (!hduLow) {
        // We read the defaults for all the HDUs we could find
        return true;
    }
    pmHDU *hduHigh = pmHDUGetHighest(fpa, chip, cell); // The HDU at the highest level
    if (!hduHigh) {
        psError(PS_ERR_UNKNOWN, true, "Can't find HDU at the highest level");
        return false;
    }
    assert(hduLow->format == hduHigh->format); // Just in case....
    psMetadata *cameraFormat = hduLow->format; // The camera format
    bool mdok = true;                   // Status of MD lookup
    psMetadata *transSpec = psMetadataLookupMetadata(&mdok, cameraFormat, "TRANSLATION"); // The TRANSLATION spec
    if (!mdok || !transSpec) {
        psError(PS_ERR_IO, true, "Failed to find \"TRANSLATION\"");
        return false;
    }

    psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
    psMetadataItem *specItem = NULL;    // Item from the specs metadata
    bool status = true;                 // Status of reading concepts
    while ((specItem = psMetadataGetAndIncrement(specsIter))) {
        pmConceptSpec *spec = specItem->data.V; // The specification
        psString name = specItem->name; // The concept name
        psMetadataItem *headerItem = NULL; // The value of the concept from the header

        // First check the cell configuration
        if (cell && cell->config) {
            psMetadataItem *conceptItem = psMetadataLookup(cell->config, name); // The concept, or NULL
            if (conceptItem) {
                // Check the SOURCE
                psString nameSource = NULL; // String with the concept name and ".SOURCE" added
                psStringAppend(&nameSource, "%s.SOURCE", name);
                psString source = psMetadataLookupStr(&mdok, cell->config, nameSource); // The source
                psFree(nameSource);
                if (mdok && strlen(source) && strcasecmp(source, "HEADER") == 0) {
                    if (hduLow->header) {
                        headerItem = psMetadataLookup(hduLow->header, conceptItem->data.V);
                    }
                    if (!headerItem && hduHigh != hduLow && hduHigh->header) {
                        headerItem = psMetadataLookup(hduHigh->header, conceptItem->data.V);
                    }
                    psMemIncrRefCounter(headerItem);
                }
                // Leave the error handling to pmConceptsFromCamera, which should already have been called
            }
        }

        if (! headerItem) {
            psString keywords = psMetadataLookupStr(&mdok, transSpec, name); // The FITS keywords
            if (mdok && strlen(keywords) > 0) {
                // In case there are multiple headers
                psList *keys = psStringSplit(keywords, " ,;", true); // List of keywords
                if (keys->n == 1) {
                    // Only one key --- proceed as usual
                    if (hduLow->header) {
                        headerItem = psMetadataLookup(hduLow->header, keywords);
                    }
                    if (!headerItem && hduHigh != hduLow && hduHigh->header) {
                        headerItem = psMetadataLookup(hduHigh->header, keywords);
                    }
                    psMemIncrRefCounter(headerItem);
                } else {
                    psListIterator *keysIter = psListIteratorAlloc(keys, PS_LIST_HEAD, false); // Iterator
                    psString key = NULL; // Item from iteration
                    psList *values = psListAlloc(NULL); // List containing the values
                    while ((key = psListGetAndIncrement(keysIter))) {
                        psMetadataItem *value = NULL;
                        if (hduLow->header) {
                            value = psMetadataLookup(hduLow->header, key);
                        }
                        if (!value && hduHigh != hduLow && hduHigh->header) {
                            value = psMetadataLookup(hduHigh->header, key);
                        }
                        if (value) {
                            psListAdd(values, PS_LIST_TAIL, value);
                        } else {
                            psWarning("Unable to find header %s --- assuming value is NULL", key);
                        }
                    }
                    psFree(keysIter);
                    headerItem = psMetadataItemAlloc(name, PS_DATA_LIST, specItem->comment, values);
                    psFree(values);
                }
                psFree(keys);
            }
        }

        // This will also clean up the name
        if (headerItem) {
            if (!conceptParse(spec, headerItem, PM_CONCEPT_SOURCE_HEADER,
                              cameraFormat, target, fpa, chip, cell)) {
                psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to parse concept %s from header.\n", name);
                status = false;
            }
        }
        psTrace("psModules.concepts", 10, "%s: %p\n", name, headerItem);
        psFree(headerItem);
    }
    psFree(specsIter);
    return status;
}


// XXX --- the below code has NOT been tested!
bool p_pmConceptsReadFromDatabase(psMetadata *target, const psMetadata *specs,
                                  const pmFPA *fpa, const pmChip *chip, const pmCell *cell, psDB *db)
{
    PS_ASSERT_PTR_NON_NULL(specs, false);
    PS_ASSERT_PTR_NON_NULL(target, false);

    #ifndef HAVE_PSDB

    return true;
    #else

    pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // The HDU at the lowest level
    if (!hdu) {
        // We read the database for all the HDUs we could find
        return true;
    }
    psMetadata *cameraFormat = hdu->format; // The camera format
    bool mdok = true;                   // Status of MD lookup
    psMetadata *dbSpec = psMetadataLookupMetadata(&mdok, cameraFormat, "DATABASE"); // The DATABASE spec
    if (!mdok || !dbSpec) {
        return true;
    }

    psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
    psMetadataItem *specItem = NULL;    // Item from the specs metadata
    bool status = true;                 // Status of reading concepts
    while ((specItem = psMetadataGetAndIncrement(specsIter))) {
        pmConceptSpec *spec = specItem->data.V; // The specification
        psString name = specItem->name; // The concept name

        psMetadata *dbLookup = psMetadataLookupMetadata(&mdok, dbSpec, name);
        if (mdok && dbLookup) {
            const char *tableName = psMetadataLookupStr(&mdok, dbLookup, "TABLE"); // Table name
            // Names of the "where" columns
            const char *givenCols = psMetadataLookupStr(&mdok, dbLookup, "GIVENDBCOL");
            // Values of the "where" columns
            const char *givenPS = psMetadataLookupStr(&mdok, dbLookup, "GIVENPS");

            // Now, need to get the "given"s
            if (strlen(givenCols) > 0 || strlen(givenPS) > 0) {
                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
                                psMetadataItem *newItem = psMetadataItemAlloc(name, item->type,
                                                          item->comment,
                                                          item->data.V);
                                psMetadataAddItem(selection, newItem, PS_LIST_TAIL, PS_META_REPLACE);
                                psFree(newItem);
                            }
                        }
                        psFree(dependName);
                        psFree(column);
                    } // Iterating through the columns
                    psFree(colsIter);
                    psFree(valuesIter);

                    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

                    psMetadataItem *conceptItem = NULL; // The final result of the DB lookup
                    if (dbResult->n == 0) {
                        psLogMsg(__func__, PS_LOG_WARN,
                                 "Unable to find any rows in DB for %s --- ignored\n", name);
                    } else {
                        if (dbResult-> n > 1) {
                            psLogMsg(__func__, PS_LOG_WARN,
                                     "Multiple rows returned in DB lookup for %s --- "
                                     " using the first one only.\n", name);
                        }
                        conceptItem = (psMetadataItem*)dbResult->data[0];
                    }

                    // Now we have the result
                    if (!conceptParse(spec, conceptItem, PM_CONCEPT_SOURCE_DATABASE,
                                      cameraFormat, target, fpa, chip, cell)) {
                        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to parse concept %s from database.\n",
                                name);
                        status = false;
                    }

                }
                psFree(cols);
                psFree(values);
            }
        } // Doing the "given"s.

    } // Iterating through the concept specifications
    psFree(specsIter);

    return status;
    #endif
}


