#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

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

#include "pmFPALevel.h"
#include "pmConcepts.h"

#include "pmConfigCamera.h"


// Remove a concept from the list of sources.  Need to check to see if it exists first, to avoid a warning.
static void removeConcept(psMetadata *source, // Source from which to remove concept
                          const char *concept // Concept name to remove
                         )
{
    assert(source);
    assert(concept && strlen(concept) > 0);

    if (psMetadataLookup(source, concept)) {
        psMetadataRemoveKey(source, concept);
    }

    return;
}

// Remove certain concepts from the list of sources.  These concepts are important in the mosaicking process,
// and are added explicitly to the defaults (elsewhere) so that the user can't get them wrong.
static void removeConceptsSources(psMetadata *source // Source for concepts
                                 )
{
    if (!source) {
        return;
    }

    removeConcept(source, "CELL.BIASSEC");
    removeConcept(source, "CELL.TRIMSEC");
    removeConcept(source, "CELL.XPARITY");
    removeConcept(source, "CELL.YPARITY");
    removeConcept(source, "CELL.X0");
    removeConcept(source, "CELL.Y0");
    removeConcept(source, "CHIP.XPARITY");
    removeConcept(source, "CHIP.YPARITY");
    removeConcept(source, "CHIP.X0");
    removeConcept(source, "CHIP.Y0");

    return;
}


// Generate a mosaicked version of a camera configuration
static bool mosaickedVersion(psMetadata *oldCameras, // Old list of camera configurations
                             psMetadata *newCameras, // New list of camera configurations
                             const char *name, // Name of original camera configuration
                             pmFPALevel level // Level
                            )
{
    assert(oldCameras);
    assert(newCameras);
    assert(name);
    assert(level == PM_FPA_LEVEL_CHIP || level == PM_FPA_LEVEL_FPA);

    // See if the old one is there
    psMetadata *camera = psMetadataLookupMetadata(NULL, oldCameras, name); // The camera configuration
    if (!camera) {
        return false;
    }

    // See if the new one is already there
    psString newName = NULL;       // Name of mosaicked camera
    psStringAppend(&newName, "_%s-%s", name, level == PM_FPA_LEVEL_CHIP ? "CHIP" : "FPA");
    if (psMetadataLookup(oldCameras, newName)) {
        return true;
    }

    psMetadata *new = psMetadataCopy(NULL, camera); // Copy of the camera
    bool mdok;                          // Status of MD lookups

    // Fix up the contents of the FPA
    psMetadata *fpa = psMetadataLookupMetadata(&mdok, new, "FPA"); // FPA in the camera configuration
    if (!mdok || !fpa) {
        psError(PS_ERR_UNEXPECTED_NULL, true, "Can't find FPA within camera configuration.");
        psFree(new);
        return NULL;
    }
    switch (level) {
    case PM_FPA_LEVEL_CHIP: {
            // Replace the contents of each chip with a single cell
            psMetadataIterator *fpaIter = psMetadataIteratorAlloc(fpa, PS_LIST_HEAD, NULL); // Iterator
            psMetadataItem *fpaItem = NULL;     // Item from iteration
            while ((fpaItem = psMetadataGetAndIncrement(fpaIter))) {
                if (fpaItem->type != PS_DATA_STRING) {
                    psWarning("Element %s within FPA in camera configuration is not of type STR.",
                              fpaItem->name);
                    continue;
                }

                psFree(fpaItem->data.str);
                fpaItem->data.str = psStringCopy("MosaickedCell");
                psFree(fpaItem->comment);
                fpaItem->comment = psStringCopy("Mosaicked cell; automatically generated");
            }
            psFree(fpaIter);
        }
        break;
    case PM_FPA_LEVEL_FPA: {
            while (psListLength(fpa->list) > 0) {
                psMetadataRemoveIndex(fpa, PS_LIST_TAIL);
            }

            psMetadataAddStr(fpa, PS_LIST_HEAD, "MosaickedChip", 0,
                             "Mosaicked chip with mosaicked cell; automatically generated",
                             "MosaickedCell");
        }
        break;
    default:
        psAbort(PS_FILE_LINE, "Should never get here.\n");
    }

    // Update the camera formats appropriately
    psMetadata *formats = psMetadataLookupMetadata(&mdok, new, "FORMATS"); // FORMATS in the configuration
    assert(mdok && formats);            // It had better be there --- we've already read them in
    psMetadataIterator *formatsIter = psMetadataIteratorAlloc(formats, PS_LIST_HEAD, NULL); // Iterator
    psMetadataItem *formatsItem = NULL; // Item from iteration
    while ((formatsItem = psMetadataGetAndIncrement(formatsIter))) {
        assert(formatsItem->type == PS_DATA_METADATA); // We should have read it by now!
        psMetadata *format = formatsItem->data.V; // The camera format

        // Add a RULE
        psMetadata *rule = psMetadataLookupMetadata(&mdok, format, "RULE"); // Way to identify format from PHU
        if (!mdok || !rule) {
            psWarning("Couldn't find RULE in the camera format %s.\n", formatsItem->name);
            continue;
        }
        switch (level) {
        case PM_FPA_LEVEL_CHIP:
            psMetadataAddStr(rule, PS_LIST_TAIL, "PSMOSAIC", 0, "Mosaicked level", "CHIP");
            break;
        case PM_FPA_LEVEL_FPA:
            psMetadataAddStr(rule, PS_LIST_TAIL, "PSMOSAIC", 0, "Mosaicked level", "FPA");
            break;
        default:
            psAbort(PS_FILE_LINE, "Should never get here.\n");
        }

        // Fix the FILE information: need to fix the levels for the PHU and EXTENSIONS.
        // If EXTENSIONS is NONE, then we need to change the CONTENT specifier to point to the chip name.
        psMetadata *file = psMetadataLookupMetadata(&mdok, format, "FILE"); // File information
        if (!mdok || !file) {
            psWarning("Couldn't find FILE in the camera format %s.\n", formatsItem->name);
            continue;
        }
        psMetadataItem *phuItem = psMetadataLookup(file, "PHU"); // PHU level
        if (!phuItem || phuItem->type != PS_DATA_STRING) {
            psWarning("Couldn't find PHU of type STR in the FILE information in the camera format %s.\n",
                      formatsItem->name);
            continue;
        }
        if (strcasecmp(phuItem->data.str, "CELL") == 0 ||
                (level == PM_FPA_LEVEL_FPA && (strcasecmp(phuItem->data.str, "CHIP") == 0))) {
            psFree(phuItem->data.str);
            if (level == PM_FPA_LEVEL_CHIP) {
                phuItem->data.str = psStringCopy("CHIP");
            } else {
                phuItem->data.str = psStringCopy("FPA");
            }
        }
        psMetadataItem *extensionsItem = psMetadataLookup(file, "EXTENSIONS"); // Extensions level
        if (!extensionsItem || extensionsItem->type != PS_DATA_STRING) {
            psWarning("Couldn't find EXTENSIONS of type STR in the FILE information "
                      "in the camera format %s.\n", formatsItem->name);
            continue;
        }
        switch (level) {
        case PM_FPA_LEVEL_CHIP:
            if (strcasecmp(extensionsItem->data.str, "NONE") == 0) {
                if (strcasecmp(phuItem->data.str, "FPA") == 0) {
                    break;              // Don't need a "CONTENT" to identify the content!
                }
                psMetadataItem *contentItem = psMetadataLookup(file, "CONTENT"); // Key to CONTENTS menu
                if (!contentItem || contentItem->type != PS_DATA_STRING) {
                    psWarning("Couldn't find CONTENT of type STR in the FILE information "
                              "in the camera format %s.\n", formatsItem->name);
                    continue;
                }
                psFree(contentItem->data.str);
                contentItem->data.str = psStringCopy("{CHIP.NAME}");
            } else if (strcasecmp(extensionsItem->data.str, "CELL") == 0) {
                psFree(extensionsItem->data.str);
                extensionsItem->data.str = psStringCopy("CHIP");
            }
            break;
        case PM_FPA_LEVEL_FPA:
            psFree(extensionsItem->data.str);
            extensionsItem->data.str = psStringCopy("NONE");
            break;
        default:
            psAbort(PS_FILE_LINE, "Should never get here.\n");
        }

        // Fix up the CONTENTS to contain only the mosaicked cell for each chip
        switch (level) {
        case PM_FPA_LEVEL_CHIP:
            if (strcasecmp(phuItem->data.str, "FPA") == 0 &&
                    strcasecmp(extensionsItem->data.str, "NONE") == 0) {
                // List the contents on a single line
                psString contentsLine = NULL; // Contents of the PHU
                psMetadataIterator *fpaIter = psMetadataIteratorAlloc(fpa, PS_LIST_HEAD, NULL); // Iterator
                psMetadataItem *fpaItem;    // Item from iteration
                while ((fpaItem = psMetadataGetAndIncrement(fpaIter))) {
                    if (fpaItem->type != PS_DATA_STRING) {
                        // We've already thrown a warning on this, above.
                        continue;
                    }
                    psStringAppend(&contentsLine, "%s:MosaickedCell:_mosaic ", fpaItem->name);
                }
                psFree(fpaIter);
                psMetadataAddStr(format, PS_LIST_TAIL, "CONTENTS", PS_META_REPLACE, NULL, contentsLine);
                psFree(contentsLine);
                break;
            }

            psMetadata *contents = psMetadataLookupMetadata(&mdok, format, "CONTENTS"); // Contents of file
            if (!mdok || !contents) {
                psWarning("Couldn't find CONTENTS in the camera format %s.\n", formatsItem->name);
                continue;
            }
            while (psListLength(contents->list) > 0) {
                psMetadataRemoveIndex(contents, PS_LIST_TAIL);
            }
            psMetadataIterator *fpaIter = psMetadataIteratorAlloc(fpa, PS_LIST_HEAD, NULL); // Iterator
            psMetadataItem *fpaItem;    // Item from iteration
            while ((fpaItem = psMetadataGetAndIncrement(fpaIter))) {
                if (fpaItem->type != PS_DATA_STRING) {
                    // We've already thrown a warning on this, above.
                    continue;
                }

                psString content = NULL;    // Content of the chip
                psStringAppend(&content, "%s:MosaickedCell:_mosaic", fpaItem->name);
                psMetadataAddStr(contents, PS_LIST_TAIL, fpaItem->name, 0, NULL, content);
                psFree(content);            // Drop reference
            }
            psFree(fpaIter);
            break;
        case PM_FPA_LEVEL_FPA:
            psMetadataAddStr(format, PS_LIST_TAIL, "CONTENTS", PS_META_REPLACE, NULL,
                             "MosaickedChip:MosaickedCell:_mosaic");
            break;
        default:
            psAbort(PS_FILE_LINE, "Should never get here.\n");
        }

        // Fix the cell type
        psMetadata *cells = psMetadataLookupMetadata(&mdok, format, "CELLS"); // CELLS information
        if (!mdok || !cells) {
            psWarning("Couldn't find CELLS of type METADATA in the camera format %s.\n", formatsItem->name);
            continue;
        }
        psMetadata *cell = psMetadataAlloc(); // Cell information
        psMetadataAddStr(cell, PS_LIST_TAIL, "CELL.TRIMSEC", 0, "Trim section", "TRIMSEC");
        psMetadataAddStr(cell, PS_LIST_TAIL, "CELL.BIASSEC", 0, "Bias section", "BIASSEC");
        psMetadataAddStr(cell, PS_LIST_TAIL, "CELL.TRIMSEC.SOURCE", 0, "Trim section source", "HEADER");
        psMetadataAddStr(cell, PS_LIST_TAIL, "CELL.BIASSEC.SOURCE", 0, "Bias section source", "HEADER");
        psMetadataAddMetadata(cells, PS_LIST_HEAD, "_mosaic", PS_META_REPLACE,
                              "Mosaic cell information", cell);
        psFree(cell);                   // Drop reference

        // Update the concepts, so that they are all stored in the FITS headers, under headers of the same
        // name as the concept
        psMetadata *database = psMetadataLookupMetadata(&mdok, format, "DATABASE"); // DATABASE concepts
        psMetadata *defaults = psMetadataLookupMetadata(&mdok, format, "DEFAULTS"); // DEFAULTS concepts
        if (!mdok || !defaults) {
            psWarning("Couldn't find DEFAULTS of type METADATA in the camera format %s.\n",
                      formatsItem->name);
            continue;
        }
        psMetadata *translation = psMetadataLookupMetadata(&mdok, format, "TRANSLATION"); // TRANSLATION info
        if (!mdok || !translation) {
            psWarning("Couldn't find TRANSLATION of type METADATA in the camera format %s.\n",
                      formatsItem->name);
            continue;
        }

        removeConceptsSources(translation);
        removeConceptsSources(database);
        removeConceptsSources(defaults);

        // Add in the positioning concepts
        psMetadataAddS32(defaults, PS_LIST_TAIL, "CHIP.XPARITY", 0, NULL, 1);
        psMetadataAddS32(defaults, PS_LIST_TAIL, "CHIP.YPARITY", 0, NULL, 1);
        psMetadataAddS32(defaults, PS_LIST_TAIL, "CELL.XPARITY", 0, NULL, 1);
        psMetadataAddS32(defaults, PS_LIST_TAIL, "CELL.YPARITY", 0, NULL, 1);
        psMetadataAddS32(defaults, PS_LIST_TAIL, "CELL.X0",      0, NULL, 0);
        psMetadataAddS32(defaults, PS_LIST_TAIL, "CELL.Y0",      0, NULL, 0);
        if (formats) {
            if (psMetadataLookup(formats, "CELL.X0")) {
                psMetadataRemoveKey(formats, "CELL.X0");
            }
            if (psMetadataLookup(formats, "CELL.Y0")) {
                psMetadataRemoveKey(formats, "CELL.Y0");
            }
        }

        if (level == PM_FPA_LEVEL_FPA) {
            psMetadataAddS32(defaults, PS_LIST_TAIL, "CHIP.X0", 0, NULL, 0);
            psMetadataAddS32(defaults, PS_LIST_TAIL, "CHIP.Y0", 0, NULL, 0);
            if (formats) {
                if (psMetadataLookup(formats, "CHIP.X0")) {
                    psMetadataRemoveKey(formats, "CHIP.X0");
                }
                if (psMetadataLookup(formats, "CHIP.Y0")) {
                    psMetadataRemoveKey(formats, "CHIP.Y0");
                }
            }
        }

    }
    psFree(formatsIter);

    // New camera MUST go to the head of the metadata, so that it will be recognised first
    // The old camera doesn't contain the PSMOSAIC header, so it will match anything!
    psMetadataAddMetadata(newCameras, PS_LIST_HEAD, newName, PS_META_REPLACE,
                          "Automatically generated", new);
    psFree(newName);
    psFree(new);

    return true;
}


// Generate the chip mosaicked version of a camera configuration
bool pmConfigCameraMosaickedVersions(psMetadata *site, // The site configuration
                                     const char *name // Name of the un-mosaicked camera
                                    )
{
    PS_ASSERT_METADATA_NON_NULL(site, false);
    PS_ASSERT_STRING_NON_EMPTY(name, false);

    bool mdok;                          // Status of MD lookup
    psMetadata *cameras = psMetadataLookupMetadata(&mdok, site, "CAMERAS"); // List of cameras
    if (!mdok || !cameras) {
        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find CAMERAS in the site configuration.\n");
        return false;
    }
    return mosaickedVersion(cameras, cameras, name, PM_FPA_LEVEL_CHIP) &&
           mosaickedVersion(cameras, cameras, name, PM_FPA_LEVEL_FPA);
}


bool pmConfigCameraMosaickedVersionsAll(psMetadata *site)
{
    PS_ASSERT_METADATA_NON_NULL(site, false);

    bool mdok;                          // Status of MD lookup
    psMetadata *cameras = psMetadataLookupMetadata(&mdok, site, "CAMERAS"); // List of cameras
    if (!mdok || !cameras) {
        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find CAMERAS in the site configuration.\n");
        return false;
    }

    psMetadataIterator *camerasIter = psMetadataIteratorAlloc(cameras, PS_LIST_HEAD, NULL); // Iterator
    psMetadataItem *camerasItem = NULL; // Item from iteration
    psMetadata *new = psMetadataAlloc();// New cameras to add
    while ((camerasItem = psMetadataGetAndIncrement(camerasIter))) {
        assert(camerasItem->type == PS_DATA_METADATA); // Only metadata are allowed here!
        mosaickedVersion(cameras, new, camerasItem->name, PM_FPA_LEVEL_CHIP);
        mosaickedVersion(cameras, new, camerasItem->name, PM_FPA_LEVEL_FPA);
    }
    psFree(camerasIter);

    // Now put the new cameras at the top of the list of cameras, so they get recognised first
    // Note: going from the top, and putting everything to the top as we get there, so that the last one on
    // goes to the top.  This preserves the original order of the cameras, putting the mosaicked versions
    // before the originals.
    camerasIter = psMetadataIteratorAlloc(new, PS_LIST_HEAD, NULL); // Iterator
    while ((camerasItem = psMetadataGetAndIncrement(camerasIter))) {
        psMetadataAddItem(cameras, camerasItem, PS_LIST_HEAD, 0);
    }
    psFree(camerasIter);
    psFree(new);

    return true;
}

