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

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

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

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

// Return the appropriate concepts metadata, given the level
static psMetadata *conceptsFromLevel(pmFPALevel level)
{
    switch (level) {
    case PM_FPA_LEVEL_FPA:
        return conceptsFPA;
    case PM_FPA_LEVEL_CHIP:
        return conceptsChip;
    case PM_FPA_LEVEL_CELL:
        return conceptsCell;
    default:
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Invalid concept level provided: %d\n", level);
        return NULL;
    }
}

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

pmConceptSpec *pmConceptSpecAlloc(psMetadataItem *blank, pmConceptParseFunc parse,
                                  pmConceptFormatFunc format, bool required)
{
    pmConceptSpec *spec = psAlloc(sizeof(pmConceptSpec));
    psMemSetDeallocator(spec, (psFreeFunc)conceptSpecFree);

    spec->blank = psMemIncrRefCounter(blank);
    spec->parse = parse;
    spec->format = format;
    spec->required = required;

    return spec;
}

psList *pmConceptsList(pmFPALevel level)
{
    if (!conceptsInitialised) {
        pmConceptsInit();
    }

    // Get the appropriate concepts
    psMetadata *concepts = conceptsFromLevel(level); // Metadata of concepts specs
    if (!concepts) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Invalid concept level provided: %d\n", level);
        return NULL;
    }

    // Pull out the names
    psList *list = psListAlloc(NULL);   // List of concepts' names
    psMetadataIterator *iter = psMetadataIteratorAlloc(concepts, PS_LIST_HEAD, NULL); // Iterator
    psMetadataItem *item;               // Item from iteration
    while ((item = psMetadataGetAndIncrement(iter))) {
        psListAdd(list, PS_LIST_TAIL, item->name);
    }
    psFree(iter);
    return list;
}

bool pmConceptGetRequired(const char *name, pmFPALevel level)
{
    PS_ASSERT_STRING_NON_EMPTY(name, false);
    if (!conceptsInitialised) {
        pmConceptsInit();
    }

    psMetadata *concepts = conceptsFromLevel(level); // The metadata of known concepts

    bool mdok;                          // Status of MD lookup
    pmConceptSpec *spec = psMetadataLookupPtr(&mdok, concepts, name); // The specification
    if (!spec) {
        // Won't throw an error, because we can't distinguish an error from the desired result.
        // However, that doesn't really matter, because if we can't find it, then it can't be required!
        return false;
    }

    return spec->required;
}

bool pmConceptSetRequired(const char *name, pmFPALevel level, bool required)
{
    PS_ASSERT_STRING_NON_EMPTY(name, false);

    if (!conceptsInitialised) {
        pmConceptsInit();
    }

    psMetadata *concepts = conceptsFromLevel(level); // The metadata of known concepts

    bool mdok;                          // Status of MD lookup
    pmConceptSpec *spec = psMetadataLookupPtr(&mdok, concepts, name); // The specification
    if (!spec) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unable to find defined concept %s in level %d.",
                name, level);
        return false;
    }
    spec->required = required;

    return true;
}

bool pmConceptRegister(psMetadataItem *blank, pmConceptParseFunc parse,
                        pmConceptFormatFunc format, bool required, pmFPALevel level)
{
    PS_ASSERT_PTR_NON_NULL(blank, false);

    if (!conceptsInitialised) {
        pmConceptsInit();
    }

    psMetadata *target = conceptsFromLevel(level); // The metadata of known concepts to write to
    if (!target) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, false,
                "Unable to register concept at invalid concept level.");
        return false;
    }

    pmConceptSpec *spec = pmConceptSpecAlloc(blank, parse, format, required); // The concept specification
    psMetadataAdd(target, PS_LIST_TAIL, blank->name, PS_DATA_UNKNOWN | PS_META_REPLACE,
                  "Concepts specification", spec);
    psFree(spec);                       // Drop reference

    return true;
}


// 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
                         )
{
    assert(specs);
    assert(target);

    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("psModules.concepts", 9, "Blanking %s...\n", specItem->name);
        pmConceptSpec *spec = specItem->data.V; // The specification
        psMetadataItem *blank = spec->blank; // The concept
        psMetadataItem *copy = NULL;    // Copy of the blank concept
        // Trap the lists, which can't be copied in the ordinary way without a warning
        if (blank->type == PS_DATA_LIST) {
            copy = psMetadataItemAllocPtr(blank->name, PS_DATA_LIST, blank->comment, blank->data.V);
        } else {
            copy = psMetadataItemCopy(blank);
        }
        if (!psMetadataAddItem(target, copy, PS_LIST_TAIL, PS_META_REPLACE)) {
            psLogMsg(__func__, PS_LOG_WARN, "Unable to add blank version of concept %s\n", blank->name);
        }
        psFree(copy);                   // Drop reference
    }
    psFree(specsIter);

    return true;
}



// Read all registered concepts for the specified level
static bool conceptsRead(psMetadata **specs, // One of the concepts specifications
                         const pmFPA *fpa,    // The FPA
                         const pmChip *chip,  // The chip
                         const pmCell *cell, // The cell
                         unsigned int *read,     // What's already been read
                         pmConceptSource source, // The source of the concepts to read
                         psDB *db,      // Database handle
                         psMetadata *target // Place into which to read the concepts
                        )
{
    assert(specs);
    assert(read);
    assert(target);

    if (!conceptsInitialised) {
        pmConceptsInit();
    }

    // At least one HDU is required for the reading functions
    pmHDU *hduLow = pmHDUGetLowest(fpa, chip, cell); // Lowest HDU.
    if (!hduLow) {
        // Can't do anything --- don't record any success, but don't return an error either
        return true;
    }
    pmHDU *hduHigh = pmHDUGetHighest(fpa, chip, cell); // Highest HDU

    bool success = true;                // Success in reading concepts?
    if (source & PM_CONCEPT_SOURCE_CELLS && !(*read & PM_CONCEPT_SOURCE_CELLS) && cell) {
        if (p_pmConceptsReadFromCells(target, *specs, cell)) {
            *read |= PM_CONCEPT_SOURCE_CELLS;
        } else {
            psError(PS_ERR_UNKNOWN, false, "Error reading concepts from camera configuration.\n");
            success = false;
        }
    }

    if (source & PM_CONCEPT_SOURCE_DEFAULTS && !(*read & PM_CONCEPT_SOURCE_DEFAULTS)) {
        if (p_pmConceptsReadFromDefaults(target, *specs, fpa, chip, cell)) {
            *read |= PM_CONCEPT_SOURCE_DEFAULTS;
        } else {
            psError(PS_ERR_UNKNOWN, false, "Error reading concepts from defaults.\n");
            success = false;
        }
    }

    if (source & PM_CONCEPT_SOURCE_PHU && !(*read & PM_CONCEPT_SOURCE_PHU) && hduHigh->header) {
        if (p_pmConceptsReadFromHeader(target, *specs, fpa, chip, cell)) {
            *read |= PM_CONCEPT_SOURCE_PHU;
        } else {
            psError(PS_ERR_UNKNOWN, false, "Error reading concepts from PHU.\n");
            success = false;
        }
    }

    // If there are multiple HDUs, then it may be that one of them hasn't been read yet (hdu->header not set)
    if (source & PM_CONCEPT_SOURCE_HEADER && !(*read & PM_CONCEPT_SOURCE_HEADER) &&
        hduLow != hduHigh && hduLow->header) {
        if (p_pmConceptsReadFromHeader(target, *specs, fpa, chip, cell)) {
            *read |= PM_CONCEPT_SOURCE_HEADER;
        } else {
            psError(PS_ERR_UNKNOWN, false, "Error reading concepts from header.\n");
            success = false;
        }
    }

    #ifdef HAVE_PSDB
    if (source & PM_CONCEPT_SOURCE_DATABASE && !(*read & PM_CONCEPT_SOURCE_DATABASE)) {
        if (p_pmConceptsReadFromDatabase(target, *specs, fpa, chip, cell, db)) {
            *read |= PM_CONCEPT_SOURCE_DATABASE;
        } else {
            psError(PS_ERR_UNKNOWN, false, "Error reading concepts from database.\n");
            success = false;
        }
    }
    #endif

    pmConceptsUpdate(fpa, chip, cell);

    return success;
}

// 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
                          psDB *db,      // Database handle
                          const psMetadata *concepts // The concepts to write out
                         )
{
    assert(specs);
    assert(concepts);

    if (!conceptsInitialised) {
        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, db, concepts);
    }

    return true;
}


bool pmConceptsRead(pmFPA *fpa, pmChip *chip, pmCell *cell, pmConceptSource source, psDB *db)
{
    PS_ASSERT_PTR_NON_NULL(fpa, false);
    bool success = conceptsRead(&conceptsFPA, fpa, chip, cell, &fpa->conceptsRead, source, db, fpa->concepts);
    if (chip) {
        success &= conceptsRead(&conceptsChip, fpa, chip, cell, &chip->conceptsRead, source, db, chip->concepts);
    }
    if (cell) {
        success &= conceptsRead(&conceptsCell, fpa, chip, cell, &cell->conceptsRead, source, db, cell->concepts);
    }

    return success;
}


bool pmConceptsBlankFPA(pmFPA *fpa)
{
    PS_ASSERT_PTR_NON_NULL(fpa, false);
    psTrace("psModules.concepts", 5, "Blanking FPA concepts: %p %p\n", conceptsFPA, fpa->concepts);
    return conceptsBlank(&conceptsFPA, fpa->concepts);
}


bool pmConceptsReadFPA(pmFPA *fpa, pmConceptSource source, bool propagateDown, psDB *db)
{
    PS_ASSERT_PTR_NON_NULL(fpa, false);
    psTrace("psModules.concepts", 5, "Reading FPA concepts: %p %p\n", conceptsFPA, fpa->concepts);
    bool success = conceptsRead(&conceptsFPA, fpa, NULL, NULL, &fpa->conceptsRead, source, db, 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) {
                success &= pmConceptsReadChip(chip, source, false, true, db);
            }
        }
    }

    return success;
}

bool pmConceptsWriteFPA(const pmFPA *fpa, pmConceptSource source, bool propagateDown, psDB *db)
{
    PS_ASSERT_PTR_NON_NULL(fpa, false);
    psTrace("psModules.concepts", 5, "Writing FPA concepts: %p %p\n", conceptsFPA, fpa->concepts);
    bool success = conceptsWrite(&conceptsFPA, fpa, NULL, NULL, source, db, 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, db);
            }
        }
    }
    return success;
}

bool pmConceptsBlankChip(pmChip *chip)
{
    PS_ASSERT_PTR_NON_NULL(chip, false);
    psTrace("psModules.concepts", 5, "Blanking chip concepts: %p %p\n", conceptsChip, chip->concepts);
    return conceptsBlank(&conceptsChip, chip->concepts);
}

bool pmConceptsReadChip(pmChip *chip, pmConceptSource source, bool propagateUp,
                        bool propagateDown, psDB *db)
{
    PS_ASSERT_PTR_NON_NULL(chip, false);
    psTrace("psModules.concepts", 5, "Reading chip concepts: %p %p\n", conceptsChip, chip->concepts);
    pmFPA *fpa = chip->parent;          // FPA to which the chip belongs
    bool success = conceptsRead(&conceptsChip, fpa, chip, NULL, &chip->conceptsRead, source, db,
                                chip->concepts);
    if (propagateUp) {
        success &= conceptsRead(&conceptsFPA, fpa, chip, NULL, &fpa->conceptsRead, source, db, 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) {
                success &= pmConceptsReadCell(cell, source, false, db);
            }
        }
    }
    return success;
}

bool pmConceptsWriteChip(const pmChip *chip, pmConceptSource source, bool propagateUp,
                         bool propagateDown, psDB *db)
{
    PS_ASSERT_PTR_NON_NULL(chip, false);
    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, db, chip->concepts);
    if (propagateUp && !fpa->hdu) {
        success &= conceptsWrite(&conceptsFPA, fpa, chip, NULL, source, db, 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, db);
            }
        }
    }
    return success;
}

bool pmConceptsBlankCell(pmCell *cell)
{
    PS_ASSERT_PTR_NON_NULL(cell, false);
    psTrace("psModules.concepts", 5, "Blanking cell concepts: %p %p\n", conceptsCell, cell->concepts);
    return conceptsBlank(&conceptsCell, cell->concepts);
}

bool pmConceptsReadCell(pmCell *cell, pmConceptSource source, bool propagateUp, psDB *db)
{
    PS_ASSERT_PTR_NON_NULL(cell, false);
    psTrace("psModules.concepts", 5, "Reading 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 = conceptsRead(&conceptsCell, fpa, chip, cell, &cell->conceptsRead, source, db,
                                cell->concepts);
    if (propagateUp) {
        success &= conceptsRead(&conceptsChip, fpa, chip, cell, &chip->conceptsRead, source, db,
                                chip->concepts);
        success &= conceptsRead(&conceptsFPA, fpa, chip, cell, &fpa->conceptsRead, source, db,
                                fpa->concepts);
    }

    return success;
}

bool pmConceptsWriteCell(const pmCell *cell, pmConceptSource source, bool propagateUp, psDB *db)
{
    PS_ASSERT_PTR_NON_NULL(cell, false);
    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, db, cell->concepts);
    if (propagateUp) {
        if (!chip->hdu) {
            success &= conceptsWrite(&conceptsChip, fpa, chip, cell, source, db, chip->concepts);
            if (!fpa->hdu) {
                success &= conceptsWrite(&conceptsFPA, fpa, chip, cell, source, db, fpa->concepts);
            }
        }
    }

    return success;
}


bool pmConceptsInit(void)
{
    conceptsInitialised = true;

    p_psMemAllocatePersistent(true);

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

        // Install the standard concepts

        // FPA.TELESCOPE
        {
            psMetadataItem *fpaTelescope = psMetadataItemAllocStr("FPA.TELESCOPE", "Camera used", "");
            pmConceptRegister(fpaTelescope, NULL, NULL, false, PM_FPA_LEVEL_FPA);
            psFree(fpaTelescope);
        }

        // FPA.INSTRUMENT
        {
            psMetadataItem *fpaInstrument = psMetadataItemAllocStr("FPA.INSTRUMENT", "Camera used", "");
            pmConceptRegister(fpaInstrument, NULL, NULL, false, PM_FPA_LEVEL_FPA);
            psFree(fpaInstrument);
        }

        // FPA.DETECTOR
        {
            psMetadataItem *fpaDetector = psMetadataItemAllocStr("FPA.DETECTOR", "Detector used", "");
            pmConceptRegister(fpaDetector, NULL, NULL, false, PM_FPA_LEVEL_FPA);
            psFree(fpaDetector);
        }

        // FPA.CAMERA
        // XXX keep or deprecate?
        {
            psMetadataItem *fpaCamera = psMetadataItemAllocStr("FPA.CAMERA", "Camera used", "");
            pmConceptRegister(fpaCamera, NULL, NULL, false, PM_FPA_LEVEL_FPA);
            psFree(fpaCamera);
        }

        // FPA.FOCUS
        {
            psMetadataItem *fpaFocus = psMetadataItemAllocF32("FPA.FOCUS", "Telescope focus", NAN);
            pmConceptRegister(fpaFocus, NULL, NULL, false, PM_FPA_LEVEL_FPA);
            psFree(fpaFocus);
        }

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

        // FPA.FILTERID
        {
            psMetadataItem *fpaFilterid = psMetadataItemAllocStr("FPA.FILTERID",
                                                                 "Filter used (parsed, abstract name) ", "");
            pmConceptRegister(fpaFilterid, (pmConceptParseFunc)p_pmConceptParse_FPA_FILTER,
                              (pmConceptFormatFunc)p_pmConceptFormat_FPA_FILTER, false, PM_FPA_LEVEL_FPA);
            psFree(fpaFilterid);
        }

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

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

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

        // FPA.RA
        {
            psMetadataItem *fpaRa = psMetadataItemAllocF64("FPA.RA", "Right Ascension of boresight", NAN);
            pmConceptRegister(fpaRa, (pmConceptParseFunc)p_pmConceptParse_FPA_Coords,
                              (pmConceptFormatFunc)p_pmConceptFormat_FPA_Coords, false, PM_FPA_LEVEL_FPA);
            psFree(fpaRa);
        }

        // FPA.DEC
        {
            psMetadataItem *fpaDec = psMetadataItemAllocF64("FPA.DEC", "Declination of boresight", NAN);
            pmConceptRegister(fpaDec, (pmConceptParseFunc)p_pmConceptParse_FPA_Coords,
                              (pmConceptFormatFunc)p_pmConceptFormat_FPA_Coords, false, PM_FPA_LEVEL_FPA);
            psFree(fpaDec);
        }

        // FPA.OBSTYPE
        {
            psMetadataItem *fpaObstype = psMetadataItemAllocStr("FPA.OBSTYPE", "Type of observation", "");
            pmConceptRegister(fpaObstype, NULL, NULL, false, PM_FPA_LEVEL_FPA);
            psFree(fpaObstype);
        }

        // FPA.OBJECT
        {
            psMetadataItem *fpaObject = psMetadataItemAllocStr("FPA.OBJECT", "Object of observation", "");
            pmConceptRegister(fpaObject, NULL, NULL, false, PM_FPA_LEVEL_FPA);
            psFree(fpaObject);
        }

        // FPA.ALT
        {
            psMetadataItem *fpaAlt = psMetadataItemAllocF64("FPA.ALT", "Altitude of telescope", NAN);
            pmConceptRegister(fpaAlt, NULL, NULL, false, PM_FPA_LEVEL_FPA);
            psFree(fpaAlt);
        }

        // FPA.AZ
        {
            psMetadataItem *fpaAz = psMetadataItemAllocF64("FPA.AZ", "Azimuth of telescope", NAN);
            pmConceptRegister(fpaAz, NULL, NULL, false, PM_FPA_LEVEL_FPA);
            psFree(fpaAz);
        }

        // FPA.TIMESYS
        {
            psMetadataItem *fpaTimesys = psMetadataItemAllocS32("FPA.TIMESYS", "Time system", -1);
            pmConceptRegister(fpaTimesys, (pmConceptParseFunc)p_pmConceptParse_TIMESYS,
                              (pmConceptFormatFunc)p_pmConceptFormat_TIMESYS, false, PM_FPA_LEVEL_FPA);
            psFree(fpaTimesys);
        }

        // FPA.TIME
        {
            psTime *time = psTimeAlloc(PS_TIME_TAI); // Blank time
            // Not particularly distinguishing, but should be good enough
            time->sec = 0;
            time->nsec = 0;
            psMetadataItem *fpaTime = psMetadataItemAlloc("FPA.TIME", PS_DATA_TIME,
                                      "Time of exposure", time);
            psFree(time);
            pmConceptRegister(fpaTime, (pmConceptParseFunc)p_pmConceptParse_TIME,
                              (pmConceptFormatFunc)p_pmConceptFormat_TIME, false, PM_FPA_LEVEL_FPA);
            psFree(fpaTime);
        }

        // FPA.TEMP
        {
            psMetadataItem *fpaTemp = psMetadataItemAllocF32("FPA.TEMP", "Temperature of focal plane", NAN);
            pmConceptRegister(fpaTemp, NULL, NULL, false, PM_FPA_LEVEL_FPA);
            psFree(fpaTemp);
        }

        // FPA.EXPOSURE
        {
            psMetadataItem *fpaExposure = psMetadataItemAllocF32("FPA.EXPOSURE",
                                          "Exposure time (sec)", NAN);
            pmConceptRegister(fpaExposure, NULL, NULL, false, PM_FPA_LEVEL_FPA);
            psFree(fpaExposure);
        }

        // Done with FPA level concepts
    }
    if (! conceptsChip) {
        conceptsChip = psMetadataAlloc();
        init = true;

        // Install the standard concepts

        // CHIP.XPARITY
        {
            psMetadataItem *chipXparity = psMetadataItemAllocS32("CHIP.XPARITY",
                                          "Orientation in x compared to the rest of the FPA", 0);
            pmConceptRegister(chipXparity, NULL, NULL, true, PM_FPA_LEVEL_CHIP);
            psFree(chipXparity);
        }

        // CHIP.YPARITY
        {
            psMetadataItem *chipYparity = psMetadataItemAllocS32("CHIP.YPARITY",
                                          "Orientation in y compared to the rest of the FPA", 0);
            pmConceptRegister(chipYparity, NULL, NULL, true, PM_FPA_LEVEL_CHIP);
            psFree(chipYparity);
        }

        // CHIP.X0
        {
            psMetadataItem *chipX0 = psMetadataItemAllocS32("CHIP.X0", "Position of (0,0) on the FPA", 0);
            pmConceptRegister(chipX0, (pmConceptParseFunc)p_pmConceptParse_Positions,
                              (pmConceptFormatFunc)p_pmConceptFormat_Positions, true, PM_FPA_LEVEL_CHIP);
            psFree(chipX0);
        }

        // CHIP.Y0
        {
            psMetadataItem *chipY0 = psMetadataItemAllocS32("CHIP.Y0", "Position of (0,0) on the FPA", 0);
            pmConceptRegister(chipY0, (pmConceptParseFunc)p_pmConceptParse_Positions,
                              (pmConceptFormatFunc)p_pmConceptFormat_Positions, true, PM_FPA_LEVEL_CHIP);
            psFree(chipY0);
        }

        // CHIP.XSIZE
        {
            psMetadataItem *chipXsize = psMetadataItemAllocS32("CHIP.XSIZE", "Size of chip (pixels)", 0);
            pmConceptRegister(chipXsize, NULL, NULL, true, PM_FPA_LEVEL_CHIP);
            psFree(chipXsize);
        }

        // CHIP.YSIZE
        {
            psMetadataItem *chipYsize = psMetadataItemAllocS32("CHIP.YSIZE", "Size of chip (pixels)", 0);
            pmConceptRegister(chipYsize, NULL, NULL, true, PM_FPA_LEVEL_CHIP);
            psFree(chipYsize);
        }

        // CHIP.TEMP
        {
            psMetadataItem *chipTemp = psMetadataItemAllocF32("CHIP.TEMP", "Temperature of chip", NAN);
            pmConceptRegister(chipTemp, NULL, NULL, false, PM_FPA_LEVEL_CHIP);
            psFree(chipTemp);
        }

        // Done with chip level concepts
    }
    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, true, PM_FPA_LEVEL_CELL);
            psFree(cellGain);
        }

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

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

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

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

        // CELL.YPARITY
        {
            psMetadataItem *cellYparity = psMetadataItemAllocS32("CELL.YPARITY",
                                          "Orientation in y compared to the rest of the chip", 0);
            pmConceptRegister(cellYparity, NULL, NULL, true, PM_FPA_LEVEL_CELL);
            psFree(cellYparity);
        }

        // CELL.READDIR
        {
            psMetadataItem *cellReaddir = psMetadataItemAllocS32("CELL.READDIR",
                                          "Read direction, rows=1, cols=2", 0);
            pmConceptRegister(cellReaddir, NULL, NULL, true, PM_FPA_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, false, PM_FPA_LEVEL_CELL);
            psFree(cellExposure);
        }

        // CELL.DARKTIME
        {
            psMetadataItem *cellDarktime = psMetadataItemAllocF32("CELL.DARKTIME",
                                           "Time since flush (sec)", NAN);
            pmConceptRegister(cellDarktime, NULL, NULL, false, PM_FPA_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_REGION,
                                          "Trim section", trimsec);
            psFree(trimsec);
            pmConceptRegister(cellTrimsec, (pmConceptParseFunc)p_pmConceptParse_CELL_TRIMSEC,
                              (pmConceptFormatFunc)p_pmConceptFormat_CELL_TRIMSEC, true, PM_FPA_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, (pmConceptParseFunc)p_pmConceptParse_CELL_BIASSEC,
                              (pmConceptFormatFunc)p_pmConceptFormat_CELL_BIASSEC, true, PM_FPA_LEVEL_CELL);
            psFree(cellBiassec);
        }

        // CELL.XBIN
        {
            psMetadataItem *cellXbin = psMetadataItemAllocS32("CELL.XBIN", "Binning in x", 0);
            pmConceptRegister(cellXbin, (pmConceptParseFunc)p_pmConceptParse_CELL_Binning,
                              (pmConceptFormatFunc)p_pmConceptFormat_CELL_XBIN, true, PM_FPA_LEVEL_CELL);
            psFree(cellXbin);
        }

        // CELL.YBIN
        {
            psMetadataItem *cellYbin = psMetadataItemAllocS32("CELL.YBIN", "Binning in y", 0);
            pmConceptRegister(cellYbin, (pmConceptParseFunc)p_pmConceptParse_CELL_Binning,
                              (pmConceptFormatFunc)p_pmConceptFormat_CELL_YBIN, true, PM_FPA_LEVEL_CELL);
            psFree(cellYbin);
        }

        // CELL.TIMESYS
        {
            psMetadataItem *cellTimesys = psMetadataItemAllocS32("CELL.TIMESYS", "Time system", -1);
            pmConceptRegister(cellTimesys, (pmConceptParseFunc)p_pmConceptParse_TIMESYS,
                              (pmConceptFormatFunc)p_pmConceptFormat_TIMESYS, false, PM_FPA_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, (pmConceptParseFunc)p_pmConceptParse_TIME,
                              (pmConceptFormatFunc)p_pmConceptFormat_TIME, false, PM_FPA_LEVEL_CELL);
            psFree(cellTime);
        }

        // CELL.X0
        {
            psMetadataItem *cellX0 = psMetadataItemAllocS32("CELL.X0", "Position of (0,0) on the chip", 0);
            pmConceptRegister(cellX0, (pmConceptParseFunc)p_pmConceptParse_Positions,
                              (pmConceptFormatFunc)p_pmConceptFormat_Positions, true, PM_FPA_LEVEL_CELL);
            psFree(cellX0);
        }

        // CELL.Y0
        {
            psMetadataItem *cellY0 = psMetadataItemAllocS32("CELL.Y0", "Position of (0,0) on the chip", 0);
            pmConceptRegister(cellY0, (pmConceptParseFunc)p_pmConceptParse_Positions,
                              (pmConceptFormatFunc)p_pmConceptFormat_Positions, true, PM_FPA_LEVEL_CELL);
            psFree(cellY0);
        }

        // CELL.XSIZE
        {
            psMetadataItem *cellXsize = psMetadataItemAllocS32("CELL.XSIZE", "Size of cell (pixels)", 0);
            pmConceptRegister(cellXsize, NULL, NULL, true, PM_FPA_LEVEL_CELL);
            psFree(cellXsize);
        }

        // CELL.YSIZE
        {
            psMetadataItem *cellYsize = psMetadataItemAllocS32("CELL.YSIZE", "Size of cell (pixels)", 0);
            pmConceptRegister(cellYsize, NULL, NULL, true, PM_FPA_LEVEL_CELL);
            psFree(cellYsize);
        }

        // CELL.XWINDOW
        {
            psMetadataItem *cellXwindow = psMetadataItemAllocS32("CELL.XWINDOW",
                                                                 "Start of cell window (pixels)", 0);
            pmConceptRegister(cellXwindow, (pmConceptParseFunc)p_pmConceptParse_Positions,
                              (pmConceptFormatFunc)p_pmConceptFormat_Positions, true, PM_FPA_LEVEL_CELL);
            psFree(cellXwindow);
        }

        // CELL.YWINDOW
        {
            psMetadataItem *cellYwindow = psMetadataItemAllocS32("CELL.YWINDOW",
                                                                 "Start of cell window (pixels)", 0);
            pmConceptRegister(cellYwindow, (pmConceptParseFunc)p_pmConceptParse_Positions,
                              (pmConceptFormatFunc)p_pmConceptFormat_Positions, true, PM_FPA_LEVEL_CELL);
            psFree(cellYwindow);
        }
    }

    p_psMemAllocatePersistent(false);

    return init;
}

void pmConceptsDone(void)
{
    psFree(conceptsFPA);
    conceptsFPA = NULL;
    psFree(conceptsChip);
    conceptsChip = NULL;
    psFree(conceptsCell);
    conceptsCell = NULL;

    conceptsInitialised = false;
}


// XXX EAM : Paul, please handle the biassec independently so the
// psList is copied without a verbose warning.  when this is fixed, you can
// convert the trace back to a log (psMetadata.c:429)

bool pmFPACopyConcepts(pmFPA *target, const pmFPA *source)
{
    PS_ASSERT_PTR_NON_NULL(target, false);
    PS_ASSERT_PTR_NON_NULL(source, false);

    // Copy FPA concepts
    target->concepts = 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 (%ld) and source (%ld) 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 = 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 (%ld) and source (%ld) 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;
            }

            psMetadataIterator *iter = psMetadataIteratorAlloc(sourceCell->concepts, PS_LIST_HEAD, NULL);
            psMetadataItem *item;       // Item from iteration
            while ((item = psMetadataGetAndIncrement(iter))) {
                psMetadataItem *new;    // New item, a copy of the old
                // Don't copy TRIMSEC or BIASSEC
                if (strcmp(item->name, "CELL.TRIMSEC") != 0 && strcmp(item->name, "CELL.BIASSEC")) {
                    new = psMetadataItemCopy(item);
                } else {
                    new = psMetadataItemAlloc(item->name, item->type, item->comment, NULL);
                }
                psMetadataAddItem(targetCell->concepts, new, PS_LIST_TAIL, PS_META_REPLACE);
                psFree(new);        // Drop reference
            }
            psFree(iter);
        }
    }

    return true;
}
