// 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 "pmConfig.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
                         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
                         pmConfig *config, // Configuration
                         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

    if (cell && (cell->conceptsRead == PM_CONCEPT_SOURCE_NONE)) {
        pmConceptsBlankCell(cell);
        cell->conceptsRead = PM_CONCEPT_SOURCE_BLANK;
    }
    if (chip && (chip->conceptsRead == PM_CONCEPT_SOURCE_NONE)) {
        pmConceptsBlankChip(chip);
        chip->conceptsRead = PM_CONCEPT_SOURCE_BLANK;
    }
    if (fpa && (fpa->conceptsRead == PM_CONCEPT_SOURCE_NONE)) {
        pmConceptsBlankFPA(fpa);
        fpa->conceptsRead = PM_CONCEPT_SOURCE_BLANK;
    }

    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, config)) {
            *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
                          pmConfig *config, // Configuration
                          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, config, concepts);
    }

    return true;
}


bool pmConceptsRead(pmFPA *fpa, pmChip *chip, pmCell *cell, pmConceptSource source, pmConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(fpa, false);
    bool success = conceptsRead(&conceptsFPA, fpa, chip, cell, &fpa->conceptsRead, source,
                                config, fpa->concepts);
    if (chip) {
        success &= conceptsRead(&conceptsChip, fpa, chip, cell, &chip->conceptsRead, source,
                                config, chip->concepts);
    }
    if (cell) {
        success &= conceptsRead(&conceptsCell, fpa, chip, cell, &cell->conceptsRead, source,
                                config, 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, pmConfig *config)
{
    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,
                                config, 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, config);
            }
        }
    }

    return success;
}

bool pmConceptsWriteFPA(const pmFPA *fpa, pmConceptSource source, bool propagateDown, pmConfig *config)
{
    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, config, 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, config);
            }
        }
    }
    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, pmConfig *config)
{
    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, config,
                                chip->concepts);
    if (propagateUp) {
        success &= conceptsRead(&conceptsFPA, fpa, chip, NULL, &fpa->conceptsRead, source,
                                config, 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, config);
            }
        }
    }
    return success;
}

bool pmConceptsWriteChip(const pmChip *chip, pmConceptSource source, bool propagateUp,
                         bool propagateDown, pmConfig *config)
{
    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, config, chip->concepts);
    if (propagateUp && !fpa->hdu) {
        success &= conceptsWrite(&conceptsFPA, fpa, chip, NULL, source, config, 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, config);
            }
        }
    }
    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, pmConfig *config)
{
    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, config,
                                cell->concepts);
    if (propagateUp) {
        success &= conceptsRead(&conceptsChip, fpa, chip, cell, &chip->conceptsRead, source, config,
                                chip->concepts);
        success &= conceptsRead(&conceptsFPA, fpa, chip, cell, &fpa->conceptsRead, source, config,
                                fpa->concepts);
    }

    return success;
}

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

    return success;
}

// Register a concept
#define CONCEPT_REGISTER_FUNCTION(TYPENAME, SUFFIX, DEFAULT) \
static void conceptRegister##SUFFIX(const char *name, /* Name of concept */ \
                                    const char *comment, /* Comment for concept */ \
                                    pmConceptParseFunc parse, /* Parsing function */ \
                                    pmConceptFormatFunc format, /* Formatting function */ \
                                    bool required, /* Required concept? */ \
                                    pmFPALevel level /* Level at which concept applies */ \
    ) \
{ \
    psMetadataItem *item = psMetadataItemAlloc##TYPENAME(name, comment, DEFAULT); /* Item to add */ \
    pmConceptRegister(item, parse, format, required, level); \
    psFree(item); \
    return; \
}

CONCEPT_REGISTER_FUNCTION(Str, Str, "");
CONCEPT_REGISTER_FUNCTION(F32, F32, NAN);
CONCEPT_REGISTER_FUNCTION(F64, F64, NAN);
CONCEPT_REGISTER_FUNCTION(S32, Enum, -1); // For enums: set default to -1
CONCEPT_REGISTER_FUNCTION(S32, S32, 0); // For values: set default to 0

static void conceptRegisterTime(const char *name, /* Name of concept */ \
                                const char *comment, /* Comment for concept */ \
                                bool required, /* Required concept? */ \
                                pmFPALevel level /* Level at which concept applies */ \
    )
{
    psTime *time = psTimeAlloc(PS_TIME_TAI); // Blank time
    // Not particularly distinguishing, but should be good enough
    time->sec = 0;
    time->nsec = 0;
    psMetadataItem *item = psMetadataItemAlloc(name, PS_DATA_TIME, comment, time);
    psFree(time);
    pmConceptRegister(item, p_pmConceptParse_TIME, p_pmConceptFormat_TIME, required, level);
    psFree(item);
}

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
        conceptRegisterStr("FPA.TELESCOPE", "Telescope of origin", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterStr("FPA.INSTRUMENT", "Instrument name (according to the instrument)", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterStr("FPA.DETECTOR", "Detector name", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterStr("FPA.COMMENT", "Observation comment", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.FOCUS", "Telescope focus", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.AIRMASS", "Airmass at boresight", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterStr("FPA.FILTERID", "Filter used (parsed, abstract name)", p_pmConceptParse_FPA_FILTER, p_pmConceptFormat_FPA_FILTER, false, PM_FPA_LEVEL_FPA);
        conceptRegisterStr("FPA.FILTER", "Filter used (instrument name)", false, NULL, NULL, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.POSANGLE", "Position angle of instrument", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterStr("FPA.RADECSYS", "Celestial coordinate system", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF64("FPA.RA", "Right Ascension of boresight", p_pmConceptParse_FPA_Coords, p_pmConceptFormat_FPA_Coords, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF64("FPA.DEC", "Declination of boresight", p_pmConceptParse_FPA_Coords, p_pmConceptFormat_FPA_Coords, false, PM_FPA_LEVEL_FPA);
        conceptRegisterStr("FPA.OBSTYPE", "Type of observation", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterStr("FPA.OBJECT", "Object of observation", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF64("FPA.ALT", "Altitude of boresight", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF64("FPA.AZ", "Azimuth of boresight", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterEnum("FPA.TIMESYS", "Time system", p_pmConceptParse_TIMESYS, p_pmConceptFormat_TIMESYS, false, PM_FPA_LEVEL_FPA);
        conceptRegisterTime("FPA.TIME", "Time of exposure", false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.TEMP", "Temperature of focal plane", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.M1X", "Primary Mirror X Position", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.M1Y", "Primary Mirror Y Position", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.M1Z", "Primary Mirror Z Position", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.M1TIP", "Primary Mirror TIP", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.M1TILT", "Primary Mirror TILT", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.M2X", "Primary Mirror X Position", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.M2Y", "Primary Mirror Y Position", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.M2Z", "Primary Mirror Z Position", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.M2TIP", "Primary Mirror TIP", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.M2TILT", "Primary Mirror TILT", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.ENV.TEMP", "Environment: Temperature", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.ENV.HUMID", "Environment: Humidity", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.ENV.WIND", "Environment: Wind speed", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.ENV.DIR", "Environment: Wind direction", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.TELTEMP.M1", "Telescope Temperatures: M1", p_pmConceptParse_TELTEMPS, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.TELTEMP.M1CELL", "Telescope Temperatures: M1 cell", p_pmConceptParse_TELTEMPS, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.TELTEMP.M2", "Telescope Temperatures: M2", p_pmConceptParse_TELTEMPS, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.TELTEMP.SPIDER", "Telescope Temperatures: spider", p_pmConceptParse_TELTEMPS, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.TELTEMP.TRUSS", "Telescope Temperatures: truss", p_pmConceptParse_TELTEMPS, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.TELTEMP.EXTRA", "Telescope Temperatures: extra", p_pmConceptParse_TELTEMPS, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.PON.TIME", "Power On Time", NULL, NULL, false, PM_FPA_LEVEL_FPA);
        conceptRegisterF32("FPA.EXPOSURE", "Exposure time (sec)", NULL, NULL, false, PM_FPA_LEVEL_FPA);
    }
    if (! conceptsChip) {
        conceptsChip = psMetadataAlloc();
        init = true;

        // Install the standard concepts
        conceptRegisterS32("CHIP.XPARITY", "Orientation in x compared to the rest of the FPA", NULL, NULL, true, PM_FPA_LEVEL_CHIP);
        conceptRegisterS32("CHIP.YPARITY", "Orientation in y compared to the rest of the FPA", NULL, NULL, true, PM_FPA_LEVEL_CHIP);
        conceptRegisterS32("CHIP.X0", "Position of (0,0) on the FPA",p_pmConceptParse_Positions,p_pmConceptFormat_Positions, true, PM_FPA_LEVEL_CHIP);
        conceptRegisterS32("CHIP.Y0", "Position of (0,0) on the FPA",p_pmConceptParse_Positions,p_pmConceptFormat_Positions, true, PM_FPA_LEVEL_CHIP);
        conceptRegisterS32("CHIP.XSIZE", "Size of chip (pixels)", NULL, NULL, true, PM_FPA_LEVEL_CHIP);
        conceptRegisterS32("CHIP.YSIZE", "Size of chip (pixels)", NULL, NULL, true, PM_FPA_LEVEL_CHIP);
        conceptRegisterF32("CHIP.TEMP", "Temperature of chip", NULL, NULL, false, PM_FPA_LEVEL_CHIP);
        conceptRegisterStr("CHIP.ID", "Chip identifier", NULL, NULL, false, PM_FPA_LEVEL_CHIP);
    }

    if (! conceptsCell) {
        conceptsCell = psMetadataAlloc();
        init = true;

        // Install the standard concepts
        conceptRegisterF32("CELL.GAIN", "CCD gain (e/count)", NULL, NULL, true, PM_FPA_LEVEL_CELL);
        conceptRegisterF32("CELL.READNOISE", "CCD read noise (e)", NULL, NULL, true, PM_FPA_LEVEL_CELL);
        conceptRegisterF32("CELL.SATURATION", "Saturation level (counts)", NULL, NULL, true, PM_FPA_LEVEL_CELL);
        conceptRegisterF32("CELL.BAD", "Bad level (counts)", NULL, NULL, true, PM_FPA_LEVEL_CELL);
        conceptRegisterS32("CELL.XPARITY", "Orientation in x compared to the rest of the chip", NULL, NULL, true, PM_FPA_LEVEL_CELL);
        conceptRegisterS32("CELL.YPARITY", "Orientation in y compared to the rest of the chip", NULL, NULL, true, PM_FPA_LEVEL_CELL);
        conceptRegisterS32("CELL.READDIR", "Read direction, rows=1, cols=2", NULL, NULL, true, PM_FPA_LEVEL_CELL);

        // 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....
        conceptRegisterF32("CELL.EXPOSURE", "Exposure time (sec)", NULL, NULL, false, PM_FPA_LEVEL_CELL);
        conceptRegisterF32("CELL.DARKTIME", "Time since flush (sec)", NULL, NULL, false, PM_FPA_LEVEL_CELL);

        conceptRegisterS32("CELL.XBIN", "Binning in x", p_pmConceptParse_CELL_Binning,p_pmConceptFormat_CELL_XBIN, true, PM_FPA_LEVEL_CELL);
        conceptRegisterS32("CELL.YBIN", "Binning in y",p_pmConceptParse_CELL_Binning,p_pmConceptFormat_CELL_YBIN, true, PM_FPA_LEVEL_CELL);
        conceptRegisterEnum("CELL.TIMESYS", "Time system", p_pmConceptParse_TIMESYS,p_pmConceptFormat_TIMESYS, false, PM_FPA_LEVEL_CELL);
        conceptRegisterTime("CELL.TIME", "Time of exposure", false, PM_FPA_LEVEL_CELL);
        conceptRegisterS32("CELL.X0", "Position of (0,0) on the chip",p_pmConceptParse_Positions,p_pmConceptFormat_Positions, true, PM_FPA_LEVEL_CELL);
        conceptRegisterS32("CELL.Y0", "Position of (0,0) on the chip",p_pmConceptParse_Positions,p_pmConceptFormat_Positions, true, PM_FPA_LEVEL_CELL);
        conceptRegisterS32("CELL.XSIZE", "Size of cell (pixels)", NULL, NULL, true, PM_FPA_LEVEL_CELL);
        conceptRegisterS32("CELL.YSIZE", "Size of cell (pixels)", NULL, NULL, true, PM_FPA_LEVEL_CELL);
        conceptRegisterS32("CELL.XWINDOW", "Start of cell window (pixels)",p_pmConceptParse_Positions,p_pmConceptFormat_Positions, true, PM_FPA_LEVEL_CELL);
        conceptRegisterS32("CELL.YWINDOW", "Start of cell window (pixels)",p_pmConceptParse_Positions,p_pmConceptFormat_Positions, true, PM_FPA_LEVEL_CELL);
        conceptRegisterF32("CELL.VARFACTOR", "Variance factor for conversion from large to small scales", NULL, NULL, true, PM_FPA_LEVEL_CELL);

        // 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, p_pmConceptParse_CELL_TRIMSEC,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, p_pmConceptParse_CELL_BIASSEC, p_pmConceptFormat_CELL_BIASSEC, true, PM_FPA_LEVEL_CELL);
            psFree(cellBiassec);
        }

    }

    p_psMemAllocatePersistent(false);

    return init;
}

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

    conceptsInitialised = false;
}


// List of concepts not to copy, for each level.
// Must be NULL-terminated
static const char *dontCopyConceptsFPA[] = { "FPA.OBS", "FPA.NAME", "FPA.CAMERA", 0 };
static const char *dontCopyConceptsChip[] = { "CHIP.NAME", 0 };
static const char *dontCopyConceptsCell[] = { "CELL.NAME", "CELL.TRIMSEC", "CELL.BIASSEC", 0 };

// Copy concepts from a source container to a target container, avoiding certain entries
static void copyConcepts(psMetadata *target, // Target metadata container
                         psMetadata *source, // Source metadata container
                         const char *dontCopyConcepts[] // Don't copy these concepts
                         )
{
    assert(target);
    assert(source);
    assert(dontCopyConcepts);

    psMetadataIterator *iter = psMetadataIteratorAlloc(source, PS_LIST_HEAD, NULL);
    psMetadataItem *item;               // Item from iteration
    while ((item = psMetadataGetAndIncrement(iter))) {
        const char *name = item->name;  // Name of concept
        bool copyOK = true;            // OK to copy
        for (int i = 0; dontCopyConcepts[i] && copyOK; i++) {
            if (!strcmp(name, dontCopyConcepts[i])) {
                copyOK = false;
            }
        }
        if (!copyOK) {
            continue;
        }
        psMetadataItem *new = psMetadataItemCopy(item); // New item, a copy of the old
        psMetadataAddItem(target, new, PS_LIST_TAIL, PS_META_REPLACE);
        psFree(new);                    // Drop reference
    }
    psFree(iter);

    return;
}


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

    // Copy FPA concepts
    copyConcepts(target->concepts, source->concepts, dontCopyConceptsFPA);

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

        copyConcepts(targetChip->concepts, sourceChip->concepts, dontCopyConceptsChip);

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

            copyConcepts(targetCell->concepts, sourceCell->concepts, dontCopyConceptsCell);
        }
    }

    return true;
}


bool pmConceptsCopyFPA(pmFPA *target, const pmFPA *source, bool chips, bool cells)
{
    PS_ASSERT_PTR_NON_NULL(target, false);
    PS_ASSERT_PTR_NON_NULL(source, false);

    // Copy FPA concepts
    copyConcepts(target->concepts, source->concepts, dontCopyConceptsFPA);

    // Copy chip concepts
    bool status = true;                 // Status of chips
    if (chips) {
        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.",
                    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;
            }

            status &= pmConceptsCopyChip(targetChip, sourceChip, cells);
        }
    }

    return status;
}

bool pmConceptsCopyChip(pmChip *target, const pmChip *source, bool cells)
{
    PS_ASSERT_PTR_NON_NULL(target, false);
    PS_ASSERT_PTR_NON_NULL(source, false);

    // Copy chip concepts
    copyConcepts(target->concepts, source->concepts, dontCopyConceptsChip);

    // Copy cell concepts
    bool status = true;                 // Status of cells
    if (cells) {
        psArray *targetCells = target->cells; // Cells in target
        psArray *sourceCells = source->cells; // Cells in source
        if (targetCells->n != sourceCells->n) {
            psError(PS_ERR_IO, true,
                    "Number of cells in target (%ld) and source (%ld) differ --- unable to copy concepts.",
                    targetCells->n, sourceCells->n);
            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;
            }

            status &= pmConceptsCopyCell(targetCell, sourceCell);
        }
    }

    return status;
}


bool pmConceptsCopyCell(pmCell *target, const pmCell *source)
{
    PS_ASSERT_PTR_NON_NULL(target, false);
    PS_ASSERT_PTR_NON_NULL(source, false);

    copyConcepts(target->concepts, source->concepts, dontCopyConceptsCell);

    return true;
}


// Interpolate the concept.  Generalises the FPA/Chip/Cell
#define CONCEPT_INTERPOLATE(SOURCE, NAME) \
    if (strncmp(concept, NAME, strlen(NAME)) == 0) { \
        if (!(SOURCE)) { \
            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Cannot interpolate %s because %s not provided", \
                    concept, NAME); \
            psFree(string); \
            return NULL; \
        } \
        psMetadataItem *item = psMetadataLookup((SOURCE)->concepts, concept); /* Item with concept */ \
        if (!item) { \
            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Can't find concept %s in %s", concept, NAME); \
            psFree(string); \
            return NULL; \
        } \
        \
        psString value = psMetadataItemParseString(item); /* Value of concept */ \
        if (!value) { \
            psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to parse concept %s", concept); \
            psFree(string); \
            return NULL; \
        } \
        \
        char replace[length + 2];       /* String to replace with value */ \
        replace[0] = '{'; \
        strcpy(replace + 1, concept); \
        strcpy(replace + length, "}"); \
        \
        psTrace("psModules.concepts", 10, "Interpolating concept %s for %s", replace, value); \
        \
        if (!psStringSubstitute(&string, value, replace)) { \
            psError(PS_ERR_UNKNOWN, false, "Unable to replace concept %s", concept); \
            psFree(string); \
            psFree(value); \
            return NULL; \
        } \
        psFree(value); \
        \
        continue; \
    }


// XXX Could make the concept delimiters, currently '{' and '}', configurable
psString pmConceptsInterpolate(const char *input,
                               const pmFPA *fpa,
                               const pmChip *chip,
                               const pmCell *cell
    )
{
    PS_ASSERT_STRING_NON_EMPTY(input, NULL);

    psString string = psStringCopy(input); // Interpolated string, to return

    char *start;                        // Start of a concept
    while ((start = strchr(string, '{'))) {
        char *stop = strchr(start, '}'); // End of a concept
        int length = stop - start;      // Length of the concept name, including terminating \0
        char concept[length];  // Name of concept
        strncpy(concept, start + 1, length - 1);
        concept[length - 1] = '\0';

        psTrace("psModules.concepts", 7, "Interpolating concept %s", concept);

        CONCEPT_INTERPOLATE(fpa,  "FPA");
        CONCEPT_INTERPOLATE(chip, "CHIP");
        CONCEPT_INTERPOLATE(cell, "CELL");

        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unrecognised concept: %s", concept);
        psFree(string);
        return NULL;
    }

    return string;
}


psMetadataItem *p_pmConceptsDepend(const char *name, const psMetadata *menu, const psMetadata *source,
                                   const pmFPA *fpa, const pmChip *chip, const pmCell *cell)
{
    psAssert(name && strlen(name) > 0, "Concept name is empty");
    psAssert(menu, "Must have menu");
    psAssert(source, "Must have source");

    // Check for DEPEND
    psString depend = NULL; // The CONCEPT.DEPEND
    psStringAppend(&depend, "%s.DEPEND", name);
    bool mdok;                          // Status of MD lookup
    const char *dependConcept = psMetadataLookupStr(&mdok, source, depend); // The concept name
    if (!mdok || !dependConcept || strlen(dependConcept) == 0) {
        psError(PS_ERR_IO, true, "Unable to parse %s: couldn't find %s in DEFAULTS.\n", name, depend);
        psFree(depend);
        return NULL;
    }
    psFree(depend);
    // Now look up the depend value
    psMetadataItem *dependValue = NULL; // The value of the concept we're looking up
    if (cell) {
        dependValue = psMetadataLookup(cell->concepts, dependConcept);
    }
    if (chip && !dependValue) {
        dependValue = psMetadataLookup(chip->concepts, dependConcept);
    }
    if (fpa && !dependValue) {
        dependValue = psMetadataLookup(chip->concepts, dependConcept);
    }
    if (!dependValue) {
        // Not an error --- it may be specified some other way
        psTrace("psModules.concepts", 7, "Couldn't find DEPEND for %s", name);
        return NULL;
    }
    if (dependValue->type != PS_DATA_STRING) {
        psError(PS_ERR_BAD_PARAMETER_TYPE, true, "%s is required to resolve %s in DEFAULTS, "
                "but it is not of type STRING.\n", dependConcept, name);
        return NULL;
    }
    const char *key = dependValue->data.V; // The key to the DEPEND menu
    psTrace("psModules.concepts", 7, "%s.DEPEND resolves to %s....\n", name, key);

    return psMetadataLookup(menu, key);
}

