// 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 "pmConcepts.h"
#include "pmConceptsRead.h"
#include "pmConceptsWrite.h"
#include "pmConceptsStandard.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, pmConceptParseFunc parse,
                                  pmConceptFormatFunc format)
{
    pmConceptSpec *spec = psAlloc(sizeof(pmConceptSpec));
    psMemSetDeallocator(spec, (psFreeFunc)conceptSpecFree);

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

    return spec;
}

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

    // Get the appropriate concepts
    psMetadata *concepts = NULL;        // Metadata of concepts specs
    switch (level) {
    case PM_FPA_LEVEL_FPA:
        concepts = conceptsFPA;
        break;
    case PM_FPA_LEVEL_CHIP:
        concepts = conceptsChip;
        break;
    case PM_FPA_LEVEL_CELL:
        concepts = conceptsCell;
        break;
    default:
        psError(PS_ERR_IO, 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;
}


psList *psMetadataKeys(psMetadata *md)
{
    psList *list = psListAlloc(NULL);   // List with the keys
    psMetadataIterator *iter = psMetadataIteratorAlloc(md, PS_LIST_HEAD, false);
    psMetadataItem *item;               // Item from iteration
    while ((item = psMetadataGetAndIncrement(iter))) {
        psListAdd(list, PS_LIST_TAIL, item->name);
    }
    return list;
}



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

    if (!conceptsInitialised) {
        pmConceptsInit();
    }

    pmConceptSpec *spec = pmConceptSpecAlloc(blank, parse, format); // The concept specification
    psMetadata **target = NULL;         // The metadata of known concepts to write to
    switch (level) {
    case PM_FPA_LEVEL_FPA:
        target = &conceptsFPA;
        break;
    case PM_FPA_LEVEL_CHIP:
        target = &conceptsChip;
        break;
    case PM_FPA_LEVEL_CELL:
        target = &conceptsCell;
        break;
    default:
        psError(PS_ERR_IO, true, "Invalid 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;
}


// 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();
    }

    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)) {
        if (!p_pmConceptsReadFromHeader(target, *specs, fpa, chip, cell)) {
            psError(PS_ERR_UNKNOWN, false, "Error reading concepts from header.\n");
            success = false;
        }
    }

    if (source & PM_CONCEPT_SOURCE_HEADER && !(*read & PM_CONCEPT_SOURCE_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

    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

        #if 0
        // FPA.NAME
        {
            psMetadataItem *fpaName = psMetadataItemAllocStr("FPA.NAME", "Name of FPA", "");
            pmConceptRegister(fpaName, NULL, NULL, PM_FPA_LEVEL_FPA);
            psFree(fpaName);
        }
        #endif

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

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

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

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

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

        // FPA.AIRMASS
        {
            psMetadataItem *fpaAirmass = psMetadataItemAllocF32("FPA.AIRMASS", "Airmass at boresight", NAN);
            pmConceptRegister(fpaAirmass, NULL, NULL, 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, PM_FPA_LEVEL_FPA);
            psFree(fpaFilterid);
        }

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

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

        // FPA.RADECSYS
        {
            psMetadataItem *fpaRadecsys = psMetadataItemAllocStr("FPA.RADECSYS",
                                          "Celestial coordinate system", "");
            pmConceptRegister(fpaRadecsys, NULL, NULL, 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, 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, PM_FPA_LEVEL_FPA);
            psFree(fpaDec);
        }

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

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

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

        // FPA.AZ
        {
            psMetadataItem *fpaAz = psMetadataItemAllocF64("FPA.AZ", "Azimuth of telescope", NAN);
            pmConceptRegister(fpaAz, NULL, NULL, 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, 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, PM_FPA_LEVEL_FPA);
            psFree(fpaTime);
        }

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

        // FPA.EXPOSURE
        {
            psMetadataItem *fpaExposure = psMetadataItemAllocF32("FPA.EXPOSURE",
                                          "Exposure time (sec)", NAN);
            pmConceptRegister(fpaExposure, NULL, NULL, 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, 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, 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, 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, PM_FPA_LEVEL_CHIP);
            psFree(chipY0);
        }

        // CHIP.TEMP
        {
            psMetadataItem *chipTemp = psMetadataItemAllocF32("CHIP.TEMP", "Temperature of chip", NAN);
            pmConceptRegister(chipTemp, NULL, NULL, 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, PM_FPA_LEVEL_CELL);
            psFree(cellGain);
        }

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

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

        // CELL.BAD
        {
            psMetadataItem *cellBad = psMetadataItemAllocF32("CELL.BAD", "Bad level (counts)", NAN);
            pmConceptRegister(cellBad, NULL, NULL, 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, 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, PM_FPA_LEVEL_CELL);
            psFree(cellYparity);
        }

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

        // CELL.DARKTIME
        {
            psMetadataItem *cellDarktime = psMetadataItemAllocF32("CELL.DARKTIME",
                                           "Time since flush (sec)", NAN);
            pmConceptRegister(cellDarktime, NULL, NULL, 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, 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, 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, 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, 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, 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, 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, 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, PM_FPA_LEVEL_CELL);
            psFree(cellY0);
        }

    }

    p_psMemAllocatePersistent(false);

    return init;
}

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

    conceptsInitialised = false;
}


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;
            }
            targetCell->concepts = psMetadataCopy(targetCell->concepts, sourceCell->concepts);
        }
    }

    return true;
}
