#include <stdio.h>

#include "pslib.h"
#include "psMetadataItemParse.h"

#include "pmFPA.h"
#include "pmHDU.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
                                  psMetadataItem *pattern // The concept 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
                         psMetadata *cameraFormat, // The camera format
                         psMetadata *target, // The target
                         pmFPA *fpa,    // The FPA
                         pmChip *chip,  // The chip
                         pmCell *cell   // The cell
                        )
{
    if (concept) {
        psMetadataItem *parsed = NULL;  // The parsed concept
        if (spec->parse) {
            parsed = spec->parse(concept, spec->blank, cameraFormat, fpa, chip, cell);
        } else {
            parsed = parsePlain(concept, spec->blank);
        }

        // Reformat so that everything's clean
        if (strcmp(spec->blank->name, parsed->name) != 0 ||
                strcmp(spec->blank->comment, parsed->comment) != 0) {
            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, concept->type, spec->blank->comment,
                                              parsed->data.V);
            }
            psFree(parsed);
            parsed = cleaned;
        }
        psMetadataAddItem(target, parsed, PS_LIST_TAIL, PS_META_REPLACE);
        psFree(parsed);     // Drop reference
        return true;
    }

    return false;
}


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

bool pmConceptsReadFromCamera(psMetadata *specs, // The concept specifications
                              pmCell *cell,  // The cell
                              psMetadata *target // Place into which to read the concepts
                             )
{
    if (cell) {
        pmHDU *hdu = pmHDUGetLowest(NULL, NULL, cell); // The HDU at the lowest level
        if (! hdu) {
            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
        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
            psMetadataItem *value = NULL; // The value of the concept
            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 && strlen(source) > 0 && strcasecmp(source, "VALUE") == 0) {
                        value = conceptItem;
                        conceptParse(spec, value, cameraFormat, target, NULL, NULL, cell);
                    } 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
                    conceptParse(spec, conceptItem, cameraFormat, target, NULL, NULL, cell);
                }
            }
        }
        psFree(specsIter);
        return true;
    }
    return false;
}


bool pmConceptsReadFromDefaults(psMetadata *specs, // The concept specifications
                                pmFPA *fpa, // The FPA
                                pmChip *chip, // The chip
                                pmCell *cell, // The cell
                                psMetadata *target // Place into which to read the concepts
                               )
{
    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 = psMetadataLookupMD(&mdok, cameraFormat, "DEFAULTS"); // The DEFAULTS spec
    if (mdok && defaults) {
        pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // The HDU at the lowest level
        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 *conceptItem = psMetadataLookup(defaults, name); // The concept, or NULL
            conceptParse(spec, conceptItem, cameraFormat, target, fpa, chip, cell);
        }
        psFree(specsIter);
        return true;
    }
    return false;
}


bool pmConceptsReadFromHeader(psMetadata *specs, // The concept specifications
                              pmFPA *fpa, // The FPA
                              pmChip *chip, // The chip
                              pmCell *cell,  // The cell
                              psMetadata *target // Place into which to read the concepts
                             )
{
    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 *transSpec = psMetadataLookupMD(&mdok, cameraFormat, "TRANSLATION"); // The TRANSLATION spec
    if (mdok && transSpec) {
        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 = 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) {
                        headerItem = psMetadataLookup(hdu->header, conceptItem->data.V);
                    }
                    // 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, " ,;"); // List of keywords
                    if (keys->n == 1) {
                        // Only one key --- proceed as usual
                        headerItem = psMetadataLookup(hdu->header, keywords);
                    } 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 = psMetadataLookup(hdu->header, key);
                            psListAdd(values, PS_LIST_TAIL, value);
                        }
                        psFree(keysIter);
                        headerItem = psMetadataItemAlloc(name, PS_DATA_LIST, specItem->comment, values);
                        psFree(values);
                    }
                    psFree(keys);
                }
            }

            // This will also clean up the name
            conceptParse(spec, headerItem, cameraFormat, target, fpa, chip, cell);
        }
        psFree(specsIter);
        return true;
    }
    return false;
}


// XXX --- the below code has NOT been tested!
bool pmConceptsReadFromDatabase(psMetadata *specs, // The concept specifications
                                pmFPA *fpa, // The FPA
                                pmChip *chip, // The chip
                                pmCell *cell,  // The cell
                                psDB *db, // The database handle
                                psMetadata *target // Place into which to read the concepts
                               )
{
    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 *dbSpec = psMetadataLookupMD(&mdok, cameraFormat, "DATABSE"); // The DATABASE spec
    if (mdok && dbSpec) {
        pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // The HDU at the lowest level
        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

            psMetadata *dbLookup = psMetadataLookupMD(&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, ",;"); // List of column names
                    psList *values = psStringSplit(givenPS, ",;"); // 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
                        conceptParse(spec, conceptItem, cameraFormat, target, fpa, chip, cell);

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

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

        return true;
    }
    return false;
}




#ifdef OLD

psMetadataItem *pmConceptReadFromCamera(pmCell *cell, // The cell
                                        const char *concept // Name of concept
                                       )
{
    if (cell) {
        psMetadata *camera = cell->config; // Camera configuration
        psMetadataItem *item = psMetadataLookup(camera, concept);
        return item;
    }
    return NULL;
}

psMetadataItem *pmConceptReadFromHeader(pmFPA *fpa, // The FPA that contains the chip
                                        pmChip *chip, // The chip that contains the cell
                                        pmCell *cell, // The cell
                                        const char *concept // Name of concept
                                       )
{
    bool mdStatus = true;               // Status of MD lookup
    psMetadata *translation = psMetadataLookupMD(&mdStatus, fpa->camera, "TRANSLATION"); // FITS translation
    if (! mdStatus) {
        psError(PS_ERR_IO, false, "Unable to find TRANSLATION in camera configuration.\n");
        return NULL;
    }

    // Look for how to translate the concept into a FITS header name
    const char *keyword = psMetadataLookupStr(&mdStatus, translation, concept);
    if (mdStatus && strlen(keyword) > 0) {
        // We have a FITS header to look up --- search each level
        if (cell && cell->hdu) {
            psMetadataItem *cellItem = psMetadataLookup(cell->hdu->header, keyword);
            if (cellItem) {
                // XXX: Need to clean up before returning
                return cellItem;
            }
        }

        if (chip && chip->hdu) {
            psMetadataItem *chipItem = psMetadataLookup(chip->hdu->header, keyword);
            if (chipItem) {
                // XXX: Need to clean up before returning
                return chipItem;
            }
        }

        if (fpa->hdu) {
            psMetadataItem *fpaItem = psMetadataLookup(fpa->hdu->header, keyword);
            if (fpaItem) {
                // XXX: Need to clean up before returning
                return fpaItem;
            }
        }
    }

    // No header value
    return NULL;
}


// Look for a default
psMetadataItem *pmConceptReadFromDefault(pmFPA *fpa, // The FPA that contains the chip
        pmChip *chip, // The chip that contains the cell
        pmCell *cell, // The cell
        const char *concept // Name of concept
                                        )
{
    bool mdOK = true;                   // Status of MD lookup
    psMetadata *defaults = psMetadataLookupMD(&mdOK, fpa->camera, "DEFAULTS");
    if (! mdOK) {
        psError(PS_ERR_IO, false, "Unable to find DEFAULTS in camera configuration.\n");
        return NULL;
    }

    psMetadataItem *defItem = psMetadataLookup(defaults, concept);
    if (defItem) {
        if (defItem->type == PS_DATA_METADATA) {
            // A dependent default
            psTrace(__func__, 7, "Evaluating dependent default....\n");
            psMetadata *dependents = defItem->data.V; // The list of dependents
            // Find out what it depends on
            psString dependName = psStringCopy(concept);
            psStringAppend(&dependName, ".DEPEND");
            psString dependsOn = psMetadataLookupStr(&mdOK, defaults, dependName);
            if (! mdOK) {
                psError(PS_ERR_IO, false, "Unable to find %s in camera configuration for dependent default"
                        " --- ignored\n", dependName);
                // XXX: Need to clean up before returning
                return NULL;
            }
            psFree(dependName);
            // Find the value of the dependent concept
            psMetadataItem *depItem = pmConceptReadFromHeader(fpa, chip, cell, dependsOn);
            if (! depItem) {
                psError(PS_ERR_IO, true, "Unable to find value for %s (required for %s)\n", dependsOn,
                        concept);
                return NULL;
            }
            if (depItem->type != PS_DATA_STRING) {
                psError(PS_ERR_IO, true, "Value of %s is not of type string, as required for dependency"
                        " --- ignored.\n", dependsOn);
            }

            defItem = psMetadataLookup(dependents, depItem->data.V);    // This is now what we were after
        }
    }

    // XXX: Need to clean up before returning
    return defItem;                     // defItem is either NULL or points to what was desired
}


// Look for a database lookup
// XXX: Not tested
psMetadataItem *pmConceptReadFromDB(pmFPA *fpa, // The FPA that contains the chip
                                    pmChip *chip, // The chip that contains the cell
                                    pmCell *cell, // The cell
                                    psDB *db, // DB handle
                                    const char *concept // Name of concept
                                   )
{
    if (! db) {
        // No database initialised
        return NULL;
    }

    bool mdStatus = true;               // Status of MD lookup
    psMetadata *database = psMetadataLookupMD(&mdStatus, fpa->camera, "DATABASE");
    if (! mdStatus) {
        // No error, because not everyone needs to use the DB
        return NULL;
    }

    psMetadata *dbLookup = psMetadataLookupMD(&mdStatus, database, concept);
    if (dbLookup) {
        const char *tableName = psMetadataLookupStr(&mdStatus, dbLookup, "TABLE"); // Name of the table
        // const char *colName = psMetadataLookupStr(&mdStatus, dbLookup, "COLUMN"); // Name of the column
        const char *givenCols = psMetadataLookupStr(&mdStatus, dbLookup, "GIVENDBCOL"); // Name of "where"
        // columns
        const char *givenPS = psMetadataLookupStr(&mdStatus, dbLookup, "GIVENPS"); // Values for "where"
        // columns

        // Now, need to get the "given"s
        if (strlen(givenCols) || strlen(givenPS)) {
            psList *cols = psStringSplit(givenCols, ",;"); // List of column names
            psList *values = psStringSplit(givenPS, ",;"); // 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", concept);
            } 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 *name = 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", concept);
                    } else {
                        // Search for the value name
                        psMetadataItem *item = pmConceptReadFromHeader(fpa, chip, cell, name);
                        if (! item) {
                            item = pmConceptReadFromDefault(fpa, chip, cell, name);
                        }
                        if (! item) {
                            psLogMsg(__func__, PS_LOG_ERROR, "Unable to find the value name %s for DB "
                                     " lookup on %s --- ignored.\n", name, concept);
                        } 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(concept, item->type,
                                                      item->comment, item->data.V);
                            psMetadataAddItem(selection, newItem, PS_LIST_TAIL, PS_META_REPLACE);
                            psFree(newItem);
                        }
                    }
                    psFree(name);
                    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 *result = 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",
                             concept);
                } else {
                    if (dbResult-> n > 1) {
                        psLogMsg(__func__, PS_LOG_WARN, "Multiple rows returned in DB lookup for %s --- "
                                 " using the first one only.\n", concept);
                    }
                    result = (psMetadataItem*)dbResult->data[0];
                }
                // XXX: Need to clean up before returning
                return result;
            }
            psFree(cols);
            psFree(values);
        }
    } // Doing the "given"s.

    psAbort(__func__, "Shouldn't ever get here.\n");
    return NULL;
}


// Concept lookup
psMetadataItem *pmConceptRead(pmFPA *fpa, // The FPA
                              pmChip *chip,// The chip
                              pmCell *cell, // The cell
                              psDB *db, // DB handle
                              const char *name // Concept name
                             )
{
    // Try headers, database, defaults in order
    psMetadataItem *item = pmConceptReadFromCamera(cell, name);
    if (! item) {
        item = pmConceptReadFromHeader(fpa, chip, cell, name);
    }
    if (! item) {
        item = pmConceptReadFromDB(fpa, chip, cell, db, name);
    }
    if (! item) {
        item = pmConceptReadFromDefault(fpa, chip, cell, name);
    }
    return item; // item is either NULL, or points to what was desired
}


#endif
