// XXX *REALLY* need generic "concept update" and "concept read" functions that handles the type transparently

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

#include "pslib.h"
#include "pmConcepts.h"
#include "pmConceptsRead.h"
#include "pmConceptsWrite.h"
#include "pmConceptsStandard.h"
#include "psAdditionals.h"

static bool conceptsInitialised = false;// Have concepts been read?
static psMetadata *conceptsFPA = NULL;  // Known concepts for FPA
static psMetadata *conceptsChip = NULL; // Known concepts for chip
static psMetadata *conceptsCell = NULL; // Known concepts for cell

// Free a concept
static void conceptSpecFree(pmConceptSpec *spec)
{
    psFree(spec->blank);
}

pmConceptSpec *pmConceptSpecAlloc(psMetadataItem *blank, // Blank value; contains the name
                                  pmConceptReadFunc read, // Function to call to read the concept
                                  pmConceptWriteFunc write // Function to call to write the concept
                                 )
{
    pmConceptSpec *spec = psAlloc(sizeof(pmConceptSpec));
    psMemSetDeallocator(spec, (psFreeFunc)conceptSpecFree);

    spec->blank = psMemIncrRefCounter(blank);
    spec->read = read;
    spec->write = write;

    return spec;
}


bool pmConceptRegister(psMetadataItem *blank, // Blank value; contains the name
                       pmConceptReadFunc read, // Function to call to read the concept
                       pmConceptWriteFunc write, // Function to call to write the concept
                       pmConceptLevel level // Level at which to store concept in the FPA hierarchy
                      )
{
    assert(blank);
    if (!conceptsInitialised) {
        pmConceptsInit();
    }

    pmConceptSpec *spec = pmConceptSpecAlloc(blank, read, write); // The concept specification
    psMetadata **target = NULL;         // The metadata of known concepts to write to
    switch (level) {
    case PM_CONCEPT_LEVEL_FPA:
        target = &conceptsFPA;
        break;
    case PM_CONCEPT_LEVEL_CHIP:
        target = &conceptsChip;
        break;
    case PM_CONCEPT_LEVEL_CELL:
        target = &conceptsCell;
        break;
    default:
        psError(PS_ERR_IO, true, "Unknown concept level provided: %d\n", level);
        psFree(spec);
        return false;
    }

    psMetadataAdd(*target, PS_LIST_TAIL, blank->name, PS_DATA_UNKNOWN | PS_META_REPLACE,
                  "Concepts specification", spec);
    psFree(spec);                       // Drop reference

    return true;
}

// This function gets called for the really boring concepts --- where all you have to do is read from a header
// or database and you don't need to muck around with conversions.
// There is no similar "writePlain", since the type is already known.
static psMetadataItem *readPlain(psMetadataItem *blank, // The blank value with name, comment, type
                                 pmFPA *fpa, // The FPA of interest
                                 pmChip *chip, // The Chip of interest
                                 pmCell *cell, // The cell of interest
                                 psDB *db // Database handle
                                )
{
    switch (blank->type) {
    case PS_DATA_STRING: {
            psString string = pmConceptReadString(fpa, chip, cell, db, blank->name);
            psMetadataItem *item = psMetadataItemAllocStr(blank->name, blank->comment, string);
            psFree(string);
            return item;
        }
    case PS_DATA_S32:
        return psMetadataItemAllocS32(blank->name, blank->comment,
                                      pmConceptReadS32(fpa, chip, cell, db, blank->name));
    case PS_DATA_F32:
        return psMetadataItemAllocF32(blank->name, blank->comment,
                                      pmConceptReadF32(fpa, chip, cell, db, blank->name));
    case PS_DATA_F64:
        return psMetadataItemAllocF64(blank->name, blank->comment,
                                      pmConceptReadF64(fpa, chip, cell, db, blank->name));
    default:
        psLogMsg(__func__, PS_LOG_WARN, "Concept %s (%s) is not of a standard type (%x)\n",
                 blank->name, blank->comment, blank->type);
    }
    return NULL;
}

// Set all registered concepts to blank value for the specified level
static bool conceptsBlank(psMetadata **specs,  // One of the concepts specifications
                          psMetadata *target // Place to install the concepts
                         )
{
    if (!conceptsInitialised) {
        pmConceptsInit();
    }
    psMetadataIterator *specsIter = psMetadataIteratorAlloc(*specs, PS_LIST_HEAD, NULL); // Iterator on specs
    psMetadataItem *specItem = NULL;    // Item from the specs metadata
    while ((specItem = psMetadataGetAndIncrement(specsIter))) {
        psTrace(__func__, 9, "Blanking %s...\n", specItem->name);
        pmConceptSpec *spec = specItem->data.V; // The specification
        psMetadataItem *blank = spec->blank; // The concept
        psMetadataAddItem(target, blank, PS_LIST_TAIL, PS_META_REPLACE);
    }
    psFree(specsIter);

    return true;
}

// Read all registered concepts for the specified level
static bool conceptsRead(psMetadata **specs, // One of the concepts specifications
                         pmFPA *fpa,    // The FPA
                         pmChip *chip,  // The chip
                         pmCell *cell,  // The cell
                         psDB *db,      // Database handle
                         psMetadata *target // Place into which to read the concepts
                        )
{
    if (!conceptsInitialised) {
        pmConceptsInit();
    }
    psMetadataIterator *specsIter = psMetadataIteratorAlloc(*specs, PS_LIST_HEAD, NULL); // Iterator on specs
    psMetadataItem *specItem = NULL;    // Item from the specs metadata
    while ((specItem = psMetadataGetAndIncrement(specsIter))) {
        pmConceptSpec *spec = specItem->data.V; // The specification
        psMetadataItem *conceptItem = NULL; // The item to add to the concepts
        if (spec->read) {
            conceptItem = spec->read(fpa, chip, cell, db);
        } else {
            conceptItem = readPlain(spec->blank, fpa, chip, cell, db);
            if (conceptItem->type != spec->blank->type) {
                psLogMsg(__func__, PS_LOG_ERROR, "Type of concept %s following read (%x) does not match "
                         "blank type (%x).\n", spec->blank->name, conceptItem->type, spec->blank->type);
            }
        }
        psMetadataAddItem(target, conceptItem, PS_LIST_TAIL, PS_META_REPLACE);
        psFree(conceptItem);            // Drop reference
    }
    psFree(specsIter);
    return true;
}

// Write all registered concepts for the specified level
static bool conceptsWrite(psMetadata **specs, // One of the concepts specifications
                          pmFPA *fpa,   // The FPA
                          pmChip *chip, // The chip
                          pmCell *cell, // The cell
                          psDB *db,      // Database handle
                          psMetadata *source // The concepts to write out
                         )
{
    if (!conceptsInitialised) {
        pmConceptsInit();
    }
    if (! fpa->camera) {
        return false;
    }
    psMetadataIterator *iter = psMetadataIteratorAlloc(source, PS_LIST_HEAD, NULL); // Iterator on concepts
    psMetadataItem *item = NULL;    // Item from the concepts
    while ((item = psMetadataGetAndIncrement(iter))) {
        const char *name = item->name;  // Name of the concept
        if (!strcmp(name, "CELL.NAME") || !strcmp(name, "CHIP.NAME")) {
            // These concepts are not written out; they are set from things like the FITS extname
            continue;
        }
        psMetadataItem *specItem = psMetadataLookup(*specs, name); // Specification for the concept
        if (specItem) {
            pmConceptSpec *spec = specItem->data.V; // The specification
            if (spec->write) {
                spec->write(fpa, chip, cell, db); // The concept
            } else {
                pmConceptWrite(fpa, chip, cell, db, source, spec->blank->name);
            }
        } else {
            psLogMsg(__func__, PS_LOG_WARN, "Unable to find specification to write concept %s in FPA\n",
                     name);
        }
    }
    psFree(iter);

    return true;
}

// Set the concepts for a given FPA to blanks
bool pmConceptsBlankFPA(pmFPA *fpa    // FPA for which to set blank concepts
                       )
{
    psTrace("psModule.concepts", 5, "Blanking FPA concepts: %x %x\n", conceptsFPA, fpa->concepts);
    return conceptsBlank(&conceptsFPA, fpa->concepts);
}

// Read the concepts for a given FPA
bool pmConceptsReadFPA(pmFPA *fpa,      // FPA for which to read concepts
                       psDB *db         // Database handle
                      )
{
    psTrace("psModule.concepts", 5, "Reading FPA concepts: %x %x\n", conceptsFPA, fpa->concepts);
    return conceptsRead(&conceptsFPA, fpa, NULL, NULL, db, fpa->concepts);
}

// Read the concepts for a given FPA
bool pmConceptsWriteFPA(pmFPA *fpa,     // FPA for which to write concepts
                        psDB *db        // Database handle
                       )
{
    psTrace("psModule.concepts", 5, "Writing FPA concepts: %x %x\n", conceptsFPA, fpa->concepts);
    return conceptsWrite(&conceptsFPA, fpa, NULL, NULL, db, fpa->concepts);
}

// Set the concepts for a given chip to blanks
bool pmConceptsBlankChip(pmChip *chip // FPA for which to set blank concepts
                        )
{
    psTrace("psModule.concepts", 5, "Blanking chip concepts: %x %x\n", conceptsChip, chip->concepts);
    return conceptsBlank(&conceptsChip, chip->concepts);
}

// Read the concepts for a given FPA
bool pmConceptsReadChip(pmChip *chip,   // Chip for which to read concepts
                        psDB *db        // Database handle
                       )
{
    psTrace("psModule.concepts", 5, "Reading chip concepts: %x %x\n", conceptsChip, chip->concepts);
    pmFPA *fpa = chip->parent;          // FPA to which the chip belongs
    return conceptsRead(&conceptsChip, fpa, chip, NULL, db, chip->concepts);
}

// Read the concepts for a given FPA
bool pmConceptsWriteChip(pmChip *chip,  // Chip for which to write concepts
                         psDB *db        // Database handle
                        )
{
    psTrace("psModule.concepts", 5, "Writing chip concepts: %x %x\n", conceptsChip, chip->concepts);
    pmFPA *fpa = chip->parent;          // FPA to which the chip belongs
    return conceptsWrite(&conceptsChip, fpa, chip, NULL, db, chip->concepts);
}

// Set the concepts for a given chip to blanks
bool pmConceptsBlankCell(pmCell *cell // Cell for which to set blank concepts
                        )
{
    psTrace("psModule.concepts", 5, "Blanking cell concepts: %x %x\n", conceptsCell, cell->concepts);
    return conceptsBlank(&conceptsCell, cell->concepts);
}

// Read the concepts for a given FPA
bool pmConceptsReadCell(pmCell *cell,   // Cell for which to read concepts
                        psDB *db        // Database handle
                       )
{
    psTrace("psModule.concepts", 5, "Writing cell concepts: %x %x\n", conceptsCell, cell->concepts);
    pmChip *chip = cell->parent;        // Chip to which the cell belongs
    pmFPA *fpa = chip->parent;          // FPA to which the chip belongs
    return conceptsRead(&conceptsCell, fpa, chip, cell, db, cell->concepts);
}

// Read the concepts for a given FPA
bool pmConceptsWriteCell(pmCell *cell,  // FPA for which to write concepts
                         psDB *db       // Database handle
                        )
{
    psTrace("psModule.concepts", 5, "Writing cell concepts: %x %x\n", conceptsCell, cell->concepts);
    pmChip *chip = cell->parent;        // Chip to which the cell belongs
    pmFPA *fpa = chip->parent;          // FPA to which the chip belongs
    return conceptsWrite(&conceptsCell, fpa, chip, cell, db, cell->concepts);
}


bool pmConceptsInit(void)
{
    bool init = false;                  // Did we initialise anything?
    if (! conceptsFPA) {
        conceptsFPA = psMetadataAlloc();
        init = true;

        // Install the standard concepts

        // FPA.NAME
        {
            psMetadataItem *fpaName = psMetadataItemAllocStr("FPA.NAME", "Name of FPA", "");
            pmConceptRegister(fpaName, NULL, NULL, PM_CONCEPT_LEVEL_FPA);
            psFree(fpaName);
        }

        // FPA.AIRMASS
        {
            psMetadataItem *fpaAirmass = psMetadataItemAllocF32("FPA.AIRMASS", "Airmass at boresight", 0.0);
            pmConceptRegister(fpaAirmass, NULL, NULL, PM_CONCEPT_LEVEL_FPA);
            psFree(fpaAirmass);
        }

        // FPA.FILTER
        {
            psMetadataItem *fpaFilter = psMetadataItemAllocStr("FPA.FILTER", "Filter used", "");
            pmConceptRegister(fpaFilter, NULL, NULL, PM_CONCEPT_LEVEL_FPA);
            psFree(fpaFilter);
        }

        // FPA.POSANGLE
        {
            psMetadataItem *fpaPosangle = psMetadataItemAllocF32("FPA.POSANGLE",
                                          "Position angle of instrument", 0.0);
            pmConceptRegister(fpaPosangle, NULL, NULL, PM_CONCEPT_LEVEL_FPA);
            psFree(fpaPosangle);
        }

        // FPA.RADECSYS
        {
            psMetadataItem *fpaRadecsys = psMetadataItemAllocStr("FPA.RADECSYS",
                                          "Celestial coordinate system", "");
            pmConceptRegister(fpaRadecsys, NULL, NULL, PM_CONCEPT_LEVEL_FPA);
            psFree(fpaRadecsys);
        }

        // FPA.RA
        {
            psMetadataItem *fpaRa = psMetadataItemAllocF64("FPA.RA", "Right Ascension of boresight", NAN);
            pmConceptRegister(fpaRa, (pmConceptReadFunc)pmConceptRead_FPA_RA,
                              (pmConceptWriteFunc)pmConceptWrite_FPA_RA, PM_CONCEPT_LEVEL_FPA);
            psFree(fpaRa);
        }

        // FPA.DEC
        {
            psMetadataItem *fpaDec = psMetadataItemAllocF64("FPA.DEC", "Declination of boresight", NAN);
            pmConceptRegister(fpaDec, (pmConceptReadFunc)pmConceptRead_FPA_DEC,
                              (pmConceptWriteFunc)pmConceptWrite_FPA_DEC, PM_CONCEPT_LEVEL_FPA);
            psFree(fpaDec);
        }

        // Done with FPA level concepts
    }
    if (! conceptsChip) {
        conceptsChip = psMetadataAlloc();
        init = true;
        // There are no standard concepts at the chip level to be installed
    }
    if (! conceptsCell) {
        conceptsCell = psMetadataAlloc();
        init = true;

        // Install the standard concepts

        // CELL.GAIN
        {
            psMetadataItem *cellGain = psMetadataItemAllocF32("CELL.GAIN", "CCD gain (e/count)", NAN);
            pmConceptRegister(cellGain, NULL, NULL, PM_CONCEPT_LEVEL_CELL);
            psFree(cellGain);
        }

        // CELL.READNOISE
        {
            psMetadataItem *cellReadnoise = psMetadataItemAllocF32("CELL.READNOISE",
                                            "CCD read noise (e)", NAN);
            pmConceptRegister(cellReadnoise, NULL, NULL, PM_CONCEPT_LEVEL_CELL);
            psFree(cellReadnoise);
        }

        // CELL.SATURATION
        {
            psMetadataItem *cellSaturation = psMetadataItemAllocF32("CELL.SATURATION",
                                             "Saturation level (counts)", NAN);
            pmConceptRegister(cellSaturation, NULL, NULL, PM_CONCEPT_LEVEL_CELL);
            psFree(cellSaturation);
        }

        // CELL.BAD
        {
            psMetadataItem *cellBad = psMetadataItemAllocF32("CELL.BAD", "Bad level (counts)", NAN);
            pmConceptRegister(cellBad, NULL, NULL, PM_CONCEPT_LEVEL_CELL);
            psFree(cellBad);
        }

        // CELL.XPARITY
        {
            psMetadataItem *cellXparity = psMetadataItemAllocS32("CELL.XPARITY",
                                          "Orientation in x compared to the rest of the FPA", 0);
            pmConceptRegister(cellXparity, NULL, NULL, PM_CONCEPT_LEVEL_CELL);
            psFree(cellXparity);
        }

        // CELL.YPARITY
        {
            psMetadataItem *cellYparity = psMetadataItemAllocS32("CELL.YPARITY",
                                          "Orientation in x compared to the rest of the FPA", 0);
            pmConceptRegister(cellYparity, NULL, NULL, PM_CONCEPT_LEVEL_CELL);
            psFree(cellYparity);
        }

        // CELL.READDIR
        {
            psMetadataItem *cellReaddir = psMetadataItemAllocS32("CELL.READDIR",
                                          "Read direction, rows=1, cols=2", 1);
            pmConceptRegister(cellReaddir, NULL, NULL, PM_CONCEPT_LEVEL_CELL);
            psFree(cellReaddir);
        }


        // These (CELL.EXPOSURE and CELL.DARKTIME) used to be READOUT.EXPOSURE and READOUT.DARKTIME, but that
        // doesn't really make sense at the moment.  Maybe we need to add a "parent" link to the readouts.
        // But then how are the exposure times REALLY derived?  They're not in the FITS headers, because a
        // readout is a plane in a 3D image.  We'll have to dream up some additional suffix to specify these,
        // but for now....

        // CELL.EXPOSURE
        {
            psMetadataItem *cellExposure = psMetadataItemAllocF32("CELL.EXPOSURE",
                                           "Exposure time (sec)", NAN);
            pmConceptRegister(cellExposure, NULL, NULL, PM_CONCEPT_LEVEL_CELL);
            psFree(cellExposure);
        }

        // CELL.DARKTIME
        {
            psMetadataItem *cellDarktime = psMetadataItemAllocF32("CELL.DARKTIME",
                                           "Time since flush (sec)", NAN);
            pmConceptRegister(cellDarktime, NULL, NULL, PM_CONCEPT_LEVEL_CELL);
            psFree(cellDarktime);
        }

        // CELL.TRIMSEC
        {
            psRegion *trimsec = psAlloc(sizeof(psRegion)); // Blank trimsec
            trimsec->x0 = trimsec->y0 = trimsec->x1 = trimsec->y1 = NAN;
            psMetadataItem *cellTrimsec = psMetadataItemAllocPtr("CELL.TRIMSEC", PS_DATA_UNKNOWN,
                                          "Trim section", trimsec);
            psFree(trimsec);
            pmConceptRegister(cellTrimsec, (pmConceptReadFunc)pmConceptRead_CELL_TRIMSEC,
                              (pmConceptWriteFunc)pmConceptWrite_CELL_TRIMSEC, PM_CONCEPT_LEVEL_CELL);
            psFree(cellTrimsec);
        }

        // CELL.BIASSEC
        {
            psList *biassecs = psListAlloc(NULL); // Blank biassecs
            psMetadataItem *cellBiassec = psMetadataItemAllocPtr("CELL.BIASSEC", PS_DATA_LIST,
                                          "Bias sections", biassecs);
            psFree(biassecs);
            pmConceptRegister(cellBiassec, (pmConceptReadFunc)pmConceptRead_CELL_BIASSEC,
                              (pmConceptWriteFunc)pmConceptWrite_CELL_BIASSEC, PM_CONCEPT_LEVEL_CELL);
            psFree(cellBiassec);
        }

        // CELL.XBIN
        {
            psMetadataItem *cellXbin = psMetadataItemAllocS32("CELL.XBIN", "Binning in x", 0);
            pmConceptRegister(cellXbin, (pmConceptReadFunc)pmConceptRead_CELL_XBIN,
                              (pmConceptWriteFunc)pmConceptWrite_CELL_XBIN, PM_CONCEPT_LEVEL_CELL);
            psFree(cellXbin);
        }

        // CELL.YBIN
        {
            psMetadataItem *cellYbin = psMetadataItemAllocS32("CELL.YBIN", "Binning in y", 0);
            pmConceptRegister(cellYbin, (pmConceptReadFunc)pmConceptRead_CELL_YBIN,
                              (pmConceptWriteFunc)pmConceptWrite_CELL_YBIN, PM_CONCEPT_LEVEL_CELL);
            psFree(cellYbin);
        }

        // CELL.TIMESYS
        {
            psMetadataItem *cellTimesys = psMetadataItemAllocS32("CELL.TIMESYS", "Time system", -1);
            pmConceptRegister(cellTimesys, (pmConceptReadFunc)pmConceptRead_CELL_TIMESYS,
                              (pmConceptWriteFunc)pmConceptWrite_CELL_TIMESYS, PM_CONCEPT_LEVEL_CELL);
            psFree(cellTimesys);
        }

        // CELL.TIME
        {
            psTime *time = psTimeAlloc(PS_TIME_TAI); // Blank time
            // Not particularly distinguishing, but should be good enough
            time->sec = 0;
            time->nsec = 0;
            psMetadataItem *cellTime = psMetadataItemAlloc("CELL.TIME", PS_DATA_TIME,
                                       "Time of exposure", time);
            psFree(time);
            pmConceptRegister(cellTime, (pmConceptReadFunc)pmConceptRead_CELL_TIME,
                              (pmConceptWriteFunc)pmConceptWrite_CELL_TIME, PM_CONCEPT_LEVEL_CELL);
            psFree(cellTime);
        }

        // CELL.X0
        {
            psMetadataItem *cellX0 = psMetadataItemAllocS32("CELL.X0", "Position of (0,0) on the chip", 0);
            pmConceptRegister(cellX0, (pmConceptReadFunc)pmConceptRead_CELL_X0,
                              (pmConceptWriteFunc)pmConceptWrite_CELL_X0, PM_CONCEPT_LEVEL_CELL);
            psFree(cellX0);
        }

        // CELL.Y0
        {
            psMetadataItem *cellY0 = psMetadataItemAllocS32("CELL.Y0", "Position of (0,0) on the chip", 0);
            pmConceptRegister(cellY0, (pmConceptReadFunc)pmConceptRead_CELL_Y0,
                              (pmConceptWriteFunc)pmConceptWrite_CELL_Y0, PM_CONCEPT_LEVEL_CELL);
            psFree(cellY0);
        }

    }

    conceptsInitialised = true;

    return init;
}

void pmConceptsDone(void)
{
    psFree(conceptsFPA);
    psFree(conceptsChip);
    psFree(conceptsCell);
}


// Copy concepts from one FPA to another
bool pmFPACopyConcepts(pmFPA *target,   // The target FPA
                       pmFPA *source    // The target FPA
                      )
{
    // Copy FPA concepts
    target->concepts = pap_psMetadataCopy(target->concepts, source->concepts);

    // Copy chip concepts
    psArray *targetChips = target->chips; // Chips in target
    psArray *sourceChips = source->chips; // Chips in source
    if (targetChips->n != sourceChips->n) {
        psError(PS_ERR_IO, true, "Number of chips in target (%d) and source (%d) differ --- unable to copy "
                "concepts.\n", targetChips->n, sourceChips->n);
        return false;
    }
    for (int i = 0; i < targetChips->n; i++) {
        pmChip *targetChip = targetChips->data[i]; // Target chip of interest
        pmChip *sourceChip = sourceChips->data[i]; // Source chip of interest
        if (! targetChip || ! sourceChip) {
            continue;
        }
        targetChip->concepts = pap_psMetadataCopy(targetChip->concepts, sourceChip->concepts);

        // Copy cell concepts
        psArray *targetCells = targetChip->cells; // Cells in target
        psArray *sourceCells = sourceChip->cells; // Cells in source
        if (targetCells->n != sourceCells->n) {
            psError(PS_ERR_IO, true, "Number of cells in target (%d) and source (%d) differ for chip %d ---"
                    " unable to copy concepts.\n", targetCells->n, sourceCells->n, i);
            return false;
        }
        for (int j = 0; j < targetCells->n; j++) {
            pmCell *targetCell = targetCells->data[j]; // Target chip of interest
            pmCell *sourceCell = sourceCells->data[j]; // Source chip of interest
            if (! targetCell || ! sourceCell) {
                continue;
            }
            targetCell->concepts = pap_psMetadataCopy(targetCell->concepts, sourceCell->concepts);
        }
    }

    return true;
}
