#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 "pmConfig.h"
#include "pmHDU.h"
#include "pmFPA.h"
#include "pmFPALevel.h"
#include "pmHDUUtils.h"
#include "pmConcepts.h"
#include "pmConceptsRead.h"

#include "pmConceptsWrite.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) {
                psWarning("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
                                     pmConceptSource source, // The concept source
                                     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, source, 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:
        psWarning("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

            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) {
                        psWarning("Concept %s is specified by header, but is not "
                                 "of type STR --- ignored.\n", conceptItem->name);
                        continue;
                    }

                    // Formatted version
                    psMetadataItem *formatted = conceptFormat(spec, conceptItem, PM_CONCEPT_SOURCE_HEADER,
                                                              cameraFormat, NULL, NULL, cell);
                    if (!formatted) {
                        continue;
                    }

                    psTrace("psModules.concepts", 8, "Writing %s to header %s\n", name, cameraItem->data.str);
                    writeHeader(hdu, cameraItem->data.V, formatted);
                    psFree(formatted);
                } else if (strcasecmp(source, "VALUE") == 0) {
                    // Formatted version
                    psMetadataItem *formatted = conceptFormat(spec, conceptItem, PM_CONCEPT_SOURCE_CELLS,
                                                              cameraFormat, NULL, NULL, cell);
                    if (!formatted) {
                        continue;
                    }

                    psTrace("psModules.concepts", 8, "Checking %s against camera format.\n", name);
                    if (! compareConcepts(formatted, cameraItem)) {
                        psWarning("Concept %s is specified by value in the camera "
                                 "format, but the values don't match.\n", name);
                    }
                    psFree(formatted);
                } else {
                    psWarning("Concept source %s isn't HEADER or VALUE --- can't "
                             "write\n", nameSource);
                }
            } else {
                // Assume it's specified by value
                psMetadataItem *formatted = conceptFormat(spec, conceptItem, PM_CONCEPT_SOURCE_CELLS,
                                                          cameraFormat, NULL, NULL, cell);
                if (!formatted) {
                    continue;
                }

                if (! compareConcepts(formatted, cameraItem)) {
                    psWarning("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, 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 *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 = p_pmConceptsReadSingleFromDefaults(name, defaults, fpa, chip, cell);
        if (!defaultItem) {
            continue;
        }
        psMetadataItem *conceptItem = psMetadataLookup(concepts, name); // The item from the concepts
        psMetadataItem *formatted = conceptFormat(spec, conceptItem, PM_CONCEPT_SOURCE_DEFAULTS,
                                                  cameraFormat, fpa, chip, cell);
        if (!formatted) {
            continue;
        }

        if (strcmp(defaultItem->name, name) != 0) {
            // Correct the name to match the concept name
            defaultItem = psMetadataItemCopy(defaultItem);
            psFree(defaultItem->name);
            defaultItem->name = psStringCopy(name);
        } else {
            psMemIncrRefCounter(defaultItem);
        }

        if (!compareConcepts(formatted, defaultItem)) {
            psWarning("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) {
            continue;
        }
        if (headerItem->type == PS_DATA_METADATA) {
            // This is a menu
            psTrace("psModules.concepts", 5, "%s is of type METADATA.\n", name);
            headerItem = p_pmConceptsDepend(name, headerItem->data.md, translation, fpa, chip, cell);
            if (!headerItem) {
                continue;
            }
        }
        if (headerItem->type != PS_DATA_STRING) {
            psWarning("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, PM_CONCEPT_SOURCE_HEADER,
                                                  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, pmConfig *config, const psMetadata *concepts)
{
    PS_ASSERT_PTR_NON_NULL(specs, false);
    PS_ASSERT_PTR_NON_NULL(concepts, false);
    PS_ASSERT_PTR_NON_NULL(config, 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 = p_pmConceptsReadSingleFromDatabase(name, database, config, fpa, chip, cell);
        if (!dbItem) {
            continue;
        }

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

        if (strcmp(dbItem->name, name) != 0) {
            // Correct the name to match the concept name
            dbItem = psMetadataItemCopy(dbItem);
            psFree(dbItem->name);
            dbItem->name = psStringCopy(name);
        } else {
            psMemIncrRefCounter(dbItem);
        }

        if (!compareConcepts(formatted, dbItem)) {
            psWarning("Concept %s is specified by the DATABASE in the camera "
                      "format, but the values don't match.\n", name);
        }
        psFree(dbItem);
        psFree(formatted);
    }
    psFree(specsIter);
    return true;
    #endif
}





// Write all registered concepts for the specified level
static bool conceptsWrite(psMetadata **specs, // One of the concepts specifications
                          const pmFPA *fpa,   // The FPA
                          const pmChip *chip, // The chip
                          const pmCell *cell, // The cell
                          pmConceptSource source, // The source of the concepts to write
                          pmConfig *config, // Configuration
                          psMetadata *concepts // The concepts to write out
                         )
{
    assert(specs);
    assert(concepts);

    pmConceptsInit();

    psTrace("psModules.concepts", 3, "Writing concepts (%p %p %p): %d\n", fpa, chip, cell, source);

    if (source & PM_CONCEPT_SOURCE_CELLS) {
        p_pmConceptsWriteToCells(*specs, cell, concepts);
    }
    if (source & PM_CONCEPT_SOURCE_DEFAULTS) {
        p_pmConceptsWriteToDefaults(*specs, fpa, chip, cell, concepts);
    }
    if (source & (PM_CONCEPT_SOURCE_PHU | PM_CONCEPT_SOURCE_HEADER)) {
        p_pmConceptsWriteToHeader(*specs, fpa, chip, cell, concepts);
    }
    if (source & PM_CONCEPT_SOURCE_DATABASE) {
        p_pmConceptsWriteToDatabase(*specs, fpa, chip, cell, config, concepts);
    }

    return true;
}


bool pmConceptsWriteFPA(const pmFPA *fpa, pmConceptSource source, bool propagateDown, pmConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(fpa, false);
    psMetadata *conceptsFPA = pmConceptsSpecs(PM_FPA_LEVEL_FPA); // Concept specifications
    psTrace("psModules.concepts", 5, "Writing FPA concepts: %p %p\n", conceptsFPA, fpa->concepts);
    bool success = conceptsWrite(&conceptsFPA, fpa, NULL, NULL, source, config, fpa->concepts);
    if (propagateDown) {
        psArray *chips = fpa->chips;        // Array of chips
        for (long i = 0; i < chips->n; i++) {
            pmChip *chip = chips->data[i];  // Chip of interest
            if (chip && !chip->hdu) {
                success &= pmConceptsWriteChip(chip, source, false, true, config);
            }
        }
    }
    return success;
}


bool pmConceptsWriteChip(const pmChip *chip, pmConceptSource source, bool propagateUp,
                         bool propagateDown, pmConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(chip, false);
    psMetadata *conceptsChip = pmConceptsSpecs(PM_FPA_LEVEL_CHIP); // Concept specifications
    psTrace("psModules.concepts", 5, "Writing chip concepts: %p %p\n", conceptsChip, chip->concepts);
    pmFPA *fpa = chip->parent;          // FPA to which the chip belongs
    bool success = conceptsWrite(&conceptsChip, fpa, chip, NULL, source, config, chip->concepts);
    if (propagateUp && !fpa->hdu) {
        psMetadata *conceptsFPA = pmConceptsSpecs(PM_FPA_LEVEL_FPA); // Concept specifications
        success &= conceptsWrite(&conceptsFPA, fpa, chip, NULL, source, config, fpa->concepts);
    }
    if (propagateDown) {
        psArray *cells = chip->cells;        // Array of cells
        for (long i = 0; i < cells->n; i++) {
            pmCell *cell = cells->data[i];  // Cell of interest
            if (cell && !cell->hdu) {
                success &= pmConceptsWriteCell(cell, source, false, config);
            }
        }
    }
    return success;
}


bool pmConceptsWriteCell(const pmCell *cell, pmConceptSource source, bool propagateUp, pmConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(cell, false);
    psMetadata *conceptsCell = pmConceptsSpecs(PM_FPA_LEVEL_CELL); // Concept specifications
    psTrace("psModules.concepts", 5, "Writing cell concepts: %p %p\n", conceptsCell, cell->concepts);
    pmChip *chip = cell->parent;        // Chip to which the cell belongs
    pmFPA *fpa = chip->parent;          // FPA to which the chip belongs

    bool success = conceptsWrite(&conceptsCell, fpa, chip, cell, source, config, cell->concepts);
    if (propagateUp) {
        if (!chip->hdu) {
            psMetadata *conceptsChip = pmConceptsSpecs(PM_FPA_LEVEL_CHIP); // Concept specifications
            success &= conceptsWrite(&conceptsChip, fpa, chip, cell, source, config, chip->concepts);
            if (!fpa->hdu) {
                psMetadata *conceptsFPA = pmConceptsSpecs(PM_FPA_LEVEL_FPA); // Concept specifications
                success &= conceptsWrite(&conceptsFPA, fpa, chip, cell, source, config, fpa->concepts);
            }
        }
    }

    return success;
}
