// 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"

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
                                  pmConceptParseFunc parse, // Function to call to parse the concept
                                  pmConceptFormatFunc format // Function to call to format the concept
                                 )
{
    pmConceptSpec *spec = psAlloc(sizeof(pmConceptSpec));
    psMemSetDeallocator(spec, (psFreeFunc)conceptSpecFree);

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

    return spec;
}


bool pmConceptRegister(psMetadataItem *blank, // Blank value; contains the name
                       pmConceptParseFunc parse, // Function to call to parse the concept
                       pmConceptFormatFunc format, // Function to call to format the concept
                       pmFPALevel level // Level at which to store concept in the FPA hierarchy
                      )
{
    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(__func__, 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
                         pmFPA *fpa,    // The FPA
                         pmChip *chip,  // The chip
                         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_CAMERA && !(*read & PM_CONCEPT_SOURCE_CAMERA)) {
        if (pmConceptsReadFromCamera(*specs, cell, target)) {
            *read |= PM_CONCEPT_SOURCE_CAMERA;
        } 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 (pmConceptsReadFromDefaults(*specs, fpa, chip, cell, target)) {
            *read |= PM_CONCEPT_SOURCE_DEFAULTS;
        } else {
            psError(PS_ERR_UNKNOWN, false, "Error reading concepts from defaults.\n");
            success = false;
        }
    }

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

    #ifndef OMIT_PSDB
    if (source & PM_CONCEPT_SOURCE_DATABASE && !(*read & PM_CONCEPT_SOURCE_DATABASE)) {
        if (pmConceptsReadFromDatabase(*specs, fpa, chip, cell, db, target)) {
            *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
                          pmFPA *fpa,   // The FPA
                          pmChip *chip, // The chip
                          pmCell *cell, // The cell
                          pmConceptSource source, // The source of the concepts to write
                          psDB *db,      // Database handle
                          psMetadata *concepts // The concepts to write out
                         )
{
    assert(specs);
    assert(concepts);

    if (!conceptsInitialised) {
        pmConceptsInit();
    }

    psTrace(__func__, 3, "Writing concepts (%x %x %x): %x\n", fpa, chip, cell, source);

    if (source & PM_CONCEPT_SOURCE_CAMERA) {
        pmConceptsWriteToCamera(*specs, cell, concepts);
    }
    if (source & PM_CONCEPT_SOURCE_DEFAULTS) {
        pmConceptsWriteToDefaults(*specs, fpa, chip, cell, concepts);
    }
    if (source & PM_CONCEPT_SOURCE_HEADER) {
        pmConceptsWriteToHeader(*specs, fpa, chip, cell, concepts);
    }
    if (source & PM_CONCEPT_SOURCE_DATABASE) {
        pmConceptsWriteToDatabase(*specs, fpa, chip, cell, db, concepts);
    }

    return true;
}

// Set the concepts for a given FPA to blanks
bool pmConceptsBlankFPA(pmFPA *fpa    // FPA for which to set blank concepts
                       )
{
    PS_ASSERT_PTR_NON_NULL(fpa, false);
    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
                       pmConceptSource source, // The source of the concepts to read
                       bool propagateDown, // Propagate to lower levels?
                       psDB *db        // Database handle
                      )
{
    PS_ASSERT_PTR_NON_NULL(fpa, false);
    psTrace("psModule.concepts", 5, "Reading FPA concepts: %x %x\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 && !chip->hdu) {
                success |= pmConceptsReadChip(chip, source, false, true, db);
            }
        }
    }

    return success;
}

// Read the concepts for a given FPA
bool pmConceptsWriteFPA(pmFPA *fpa,     // FPA for which to write concepts
                        pmConceptSource source, // The source of the concepts to read
                        bool propagateDown, // Propagate to lower levels?
                        psDB *db        // Database handle
                       )
{
    PS_ASSERT_PTR_NON_NULL(fpa, false);
    psTrace("psModule.concepts", 5, "Writing FPA concepts: %x %x\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;
}

// Set the concepts for a given chip to blanks
bool pmConceptsBlankChip(pmChip *chip // FPA for which to set blank concepts
                        )
{
    PS_ASSERT_PTR_NON_NULL(chip, false);
    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
                        pmConceptSource source, // The source of the concepts to read
                        bool propagateUp, // Propagate to higher levels?
                        bool propagateDown, // Propagate to lower levels?
                        psDB *db        // Database handle
                       )
{
    PS_ASSERT_PTR_NON_NULL(chip, false);
    psTrace("psModule.concepts", 5, "Reading chip concepts: %x %x\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 && !fpa->hdu) {
        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 && !cell->hdu) {
                success |= pmConceptsReadCell(cell, source, false, db);
            }
        }
    }
    return success;
}

// Read the concepts for a given FPA
bool pmConceptsWriteChip(pmChip *chip,  // Chip for which to write concepts
                         pmConceptSource source, // The source of the concepts to read
                         bool propagateUp, // Propagate to higher levels?
                         bool propagateDown, // Propagate to lower levels?
                         psDB *db        // Database handle
                        )
{
    PS_ASSERT_PTR_NON_NULL(chip, false);
    psTrace("psModule.concepts", 5, "Writing chip concepts: %x %x\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;
}

// Set the concepts for a given chip to blanks
bool pmConceptsBlankCell(pmCell *cell // Cell for which to set blank concepts
                        )
{
    PS_ASSERT_PTR_NON_NULL(cell, false);
    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
                        pmConceptSource source, // The source of the concepts to read
                        bool propagateUp, // Propagate to higher levels?
                        psDB *db        // Database handle
                       )
{
    PS_ASSERT_PTR_NON_NULL(cell, false);
    psTrace("psModule.concepts", 5, "Reading 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

    bool success = conceptsRead(&conceptsCell, fpa, chip, cell, &cell->conceptsRead, source, db,
                                cell->concepts);
    if (propagateUp) {
        if (!chip->hdu) {
            success |= conceptsRead(&conceptsChip, fpa, chip, cell, &chip->conceptsRead, source, db,
                                    chip->concepts);
            if (!fpa->hdu) {
                success |= conceptsRead(&conceptsFPA, fpa, chip, cell, &fpa->conceptsRead, source, db,
                                        fpa->concepts);
            }
        }
    }

    return success;
}

// Read the concepts for a given FPA
bool pmConceptsWriteCell(pmCell *cell,  // FPA for which to write concepts
                         pmConceptSource source, // The source of the concepts to read
                         bool propagateUp, // Propagate to higher levels?
                         psDB *db       // Database handle
                        )
{
    PS_ASSERT_PTR_NON_NULL(cell, false);
    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

    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;

    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.CAMERA
        {
            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.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);
        }

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

        // 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 x 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_CELL_TIMESYS,
                              (pmConceptFormatFunc)p_pmConceptFormat_CELL_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_CELL_TIME,
                              (pmConceptFormatFunc)p_pmConceptFormat_CELL_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);
        }

    }

    return init;
}

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

    conceptsInitialised = false;
}


// Copy concepts from one FPA to another
bool pmFPACopyConcepts(pmFPA *target,   // The target FPA
                       pmFPA *source    // The target FPA
                      )
{
    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 (%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 = 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 = psMetadataCopy(targetCell->concepts, sourceCell->concepts);
        }
    }

    return true;
}
