#include <stdio.h>
#include <strings.h>
#include <assert.h>
#include "pslib.h"

#include "pmAstrometry.h"
#include "pmFPAConceptsGet.h"

//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Private functions
//////////////////////////////////////////////////////////////////////////////////////////////////////////////

psMetadataItem *p_pmFPAConceptGetFromCamera(pmCell *cell, // The cell
        const char *concept // Name of concept
                                           )
{
    if (cell) {
        psMetadata *camera = cell->camera;      // Camera data
        psMetadataItem *item = psMetadataLookup(camera, concept);
        return item;
    }
    return NULL;
}


psMetadataItem *p_pmFPAConceptGetFromHeader(pmFPA *fpa, // The FPA that contains the chip
        pmChip *chip, // The chip that contains the cell
        pmCell *cell, // The cell
        const char *concept // Name of concept
                                           )
{
    bool mdStatus = true;               // Status of MD lookup
    psMetadata *translation = psMetadataLookupMD(&mdStatus, fpa->camera, "TRANSLATION"); // FITS translation
    if (! mdStatus) {
        psError(PS_ERR_IO, false, "Unable to find TRANSLATION in camera configuration.\n");
        return NULL;
    }

    // Look for how to translate the concept into a FITS header name
    const char *keyword = psMetadataLookupStr(&mdStatus, translation, concept);
    if (mdStatus && strlen(keyword) > 0) {
        // We have a FITS header to look up --- search each level
        if (cell && cell->hdu) {
            psMetadataItem *cellItem = psMetadataLookup(cell->hdu->header, keyword);
            if (cellItem) {
                // XXX: Need to clean up before returning
                return cellItem;
            }
        }

        if (chip && chip->hdu) {
            psMetadataItem *chipItem = psMetadataLookup(chip->hdu->header, keyword);
            if (chipItem) {
                // XXX: Need to clean up before returning
                return chipItem;
            }
        }

        if (fpa->hdu) {
            psMetadataItem *fpaItem = psMetadataLookup(fpa->hdu->header, keyword);
            if (fpaItem) {
                // XXX: Need to clean up before returning
                return fpaItem;
            }
        }

        if (fpa->phu) {
            psMetadataItem *fpaItem = psMetadataLookup(fpa->phu, keyword);
            if (fpaItem) {
                // XXX: Need to clean up before returning
                return fpaItem;
            }
        }
    }

    // No header value
    return NULL;
}


// Look for a default
psMetadataItem *p_pmFPAConceptGetFromDefault(pmFPA *fpa, // The FPA that contains the chip
        pmChip *chip, // The chip that contains the cell
        pmCell *cell, // The cell
        const char *concept // Name of concept
                                            )
{
    bool mdOK = true;                   // Status of MD lookup
    psMetadata *defaults = psMetadataLookupMD(&mdOK, fpa->camera, "DEFAULTS");
    if (! mdOK) {
        psError(PS_ERR_IO, false, "Unable to find DEFAULTS in camera configuration.\n");
        return NULL;
    }

    psMetadataItem *defItem = psMetadataLookup(defaults, concept);
    if (defItem) {
        if (defItem->type == PS_DATA_METADATA) {
            // A dependent default
            psTrace(__func__, 7, "Evaluating dependent default....\n");
            psMetadata *dependents = defItem->data.V; // The list of dependents
            // Find out what it depends on
            psString dependName = psStringCopy(concept);
            psStringAppend(&dependName, ".DEPEND");
            psString dependsOn = psMetadataLookupStr(&mdOK, defaults, dependName);
            if (! mdOK) {
                psError(PS_ERR_IO, false, "Unable to find %s in camera configuration for dependent default"
                        " --- ignored\n", dependName);
                // XXX: Need to clean up before returning
                return NULL;
            }
            psFree(dependName);
            // Find the value of the dependent concept
            psMetadataItem *depItem = p_pmFPAConceptGetFromHeader(fpa, chip, cell, dependsOn);
            if (! depItem) {
                psError(PS_ERR_IO, true, "Unable to find value for %s (required for %s)\n", dependsOn,
                        concept);
                return NULL;
            }
            if (depItem->type != PS_DATA_STRING) {
                psError(PS_ERR_IO, true, "Value of %s is not of type string, as required for dependency"
                        " --- ignored.\n", dependsOn);
            }

            defItem = psMetadataLookup(dependents, depItem->data.V);    // This is now what we were after
        }
    }

    // XXX: Need to clean up before returning
    return defItem;                     // defItem is either NULL or points to what was desired
}


// Look for a database lookup
// XXX: Not tested
psMetadataItem *p_pmFPAConceptGetFromDB(pmFPA *fpa, // The FPA that contains the chip
                                        pmChip *chip, // The chip that contains the cell
                                        pmCell *cell, // The cell
                                        psDB *db,       // DB handle
                                        const char *concept // Name of concept
                                       )
{
    if (! db) {
        // No database initialised
        return NULL;
    }

    bool mdStatus = true;               // Status of MD lookup
    psMetadata *database = psMetadataLookupMD(&mdStatus, fpa->camera, "DATABASE");
    if (! mdStatus) {
        // No error, because not everyone needs to use the DB
        return NULL;
    }

    psMetadata *dbLookup = psMetadataLookupMD(&mdStatus, database, concept);
    if (dbLookup) {
        const char *tableName = psMetadataLookupStr(&mdStatus, dbLookup, "TABLE"); // Name of the table
        //        const char *colName = psMetadataLookupStr(&mdStatus, dbLookup, "COLUMN"); // Name of the column
        const char *givenCols = psMetadataLookupStr(&mdStatus, dbLookup, "GIVENDBCOL"); // Name of "where"
        // columns
        const char *givenPS = psMetadataLookupStr(&mdStatus, dbLookup, "GIVENPS"); // Values for "where"
        // columns

        // Now, need to get the "given"s
        if (strlen(givenCols) || strlen(givenPS)) {
            psList *cols = psStringSplit(givenCols, ",;"); // List of column names
            psList *values = psStringSplit(givenPS, ",;"); // List of value names for the columns
            psMetadata *selection = psMetadataAlloc(); // The stuff to select in the DB
            if (cols->n != values->n) {
                psLogMsg(__func__, PS_LOG_WARN, "The GIVENDBCOL and GIVENPS entries for %s do not have "
                         "the same number of entries --- ignored.\n", concept);
            } else {
                // Iterators for the lists
                psListIterator *colsIter = psListIteratorAlloc(cols, PS_LIST_HEAD, false);
                psListIterator *valuesIter = psListIteratorAlloc(values, PS_LIST_HEAD, false);
                char *column = NULL;    // Name of the column
                while ((column = psListGetAndIncrement(colsIter))) {
                    char *name = psListGetAndIncrement(valuesIter); // Name for the value
                    if (!strlen(column) || !strlen(name)) {
                        psLogMsg(__func__, PS_LOG_WARN, "One of the columns or value names for %s is "
                                 " empty --- ignored.\n", concept);
                    } else {
                        // Search for the value name
                        psMetadataItem *item = p_pmFPAConceptGetFromHeader(fpa, chip, cell, name);
                        if (! item) {
                            item = p_pmFPAConceptGetFromDefault(fpa, chip, cell, name);
                        }
                        if (! item) {
                            psLogMsg(__func__, PS_LOG_ERROR, "Unable to find the value name %s for DB "
                                     " lookup on %s --- ignored.\n", name, concept);
                        } else {
                            // We need to create a new psMetadataItem.  I don't think we can't simply hack
                            // the existing one, since that could conceivably cause memory leaks
                            psMetadataItem *newItem = psMetadataItemAlloc(concept, item->type,
                                                      item->comment, item->data.V);
                            psMetadataAddItem(selection, newItem, PS_LIST_TAIL, PS_META_REPLACE);
                            psFree(newItem);
                        }
                    }
                    psFree(name);
                    psFree(column);
                } // Iterating through the columns
                psFree(colsIter);
                psFree(valuesIter);

                psArray *dbResult = psDBSelectRows(db, tableName, selection, 2); // Lookup result
                // Note that we use limit=2 in order to test if there are multiple rows returned

                psMetadataItem *result = NULL; // The final result of the DB lookup
                if (dbResult->n == 0) {
                    psLogMsg(__func__, PS_LOG_WARN, "Unable to find any rows in DB for %s --- ignored\n",
                             concept);
                } else {
                    if (dbResult-> n > 1) {
                        psLogMsg(__func__, PS_LOG_WARN, "Multiple rows returned in DB lookup for %s --- "
                                 " using the first one only.\n", concept);
                    }
                    result = (psMetadataItem*)dbResult->data[0];
                }
                // XXX: Need to clean up before returning
                return result;
            }
            psFree(cols);
            psFree(values);
        }
    } // Doing the "given"s.

    psAbort(__func__, "Shouldn't ever get here.\n");
    return NULL;
}


// Concept lookup
psMetadataItem *p_pmFPAConceptGet(pmFPA *fpa, // The FPA
                                  pmChip *chip,// The chip
                                  pmCell *cell, // The cell
                                  psDB *db, // DB handle
                                  const char *concept // Concept name
                                 )
{
    // Try headers, database, defaults in order
    psMetadataItem *item = p_pmFPAConceptGetFromCamera(cell, concept);
    if (! item) {
        item = p_pmFPAConceptGetFromHeader(fpa, chip, cell, concept);
    }
    if (! item) {
        item = p_pmFPAConceptGetFromDB(fpa, chip, cell, db, concept);
    }
    if (! item) {
        item = p_pmFPAConceptGetFromDefault(fpa, chip, cell, concept);
    }
    return item; // item is either NULL, or points to what was desired
}


void p_pmFPAConceptGetF32(pmFPA *fpa,   // The FPA
                          pmChip *chip, // The chip
                          pmCell *cell, // The cell
                          psDB *db,     // DB handle
                          psMetadata *concepts, // The concepts MD
                          const char *name, // Name of the concept
                          const char *comment // Comment for concept
                         )
{
    psMetadataItem *item = p_pmFPAConceptGet(fpa, chip, cell, db, name);
    float value = NAN;
    if (item) {
        switch (item->type) {
        case PS_DATA_F32:
            value = item->data.F32;
            break;
        case PS_DATA_F64:
            // Assume it's OK to truncate to floating point from double
            value = (float)item->data.F64;
            break;
        case PS_DATA_S32:
            // Promote to float
            value = (float)item->data.S32;
            break;
        default:
            psError(PS_ERR_IO, true, "Concept %s (%s) is not of floating point type (%x) --- treating as "
                    "undefined.\n", name, comment, item->type);
        }
    } else {
        psError(PS_ERR_IO, true, "Concept %s (%s) is not defined.\n", name, comment);
    }
    psTrace(__func__, 7, "Adding %s (%s): %f\n", name, comment, value);

    psMetadataAdd(concepts, PS_LIST_TAIL, name, PS_TYPE_F32 | PS_META_REPLACE, comment, value);
}

void p_pmFPAConceptGetF64(pmFPA *fpa,   // The FPA
                          pmChip *chip, // The chip
                          pmCell *cell, // The cell
                          psDB *db,     // DB handle
                          psMetadata *concepts, // The concepts MD
                          const char *name, // Name of the concept
                          const char *comment // Comment for concept
                         )
{
    psMetadataItem *item = p_pmFPAConceptGet(fpa, chip, cell, db, name);
    double value = NAN;
    if (item) {
        switch (item->type) {
        case PS_TYPE_F64:
            value = item->data.F64;
            break;
        case PS_TYPE_F32:
            // Promote to double
            value = (double)item->data.F32;
            break;
        case PS_TYPE_S32:
            // Promote to double
            value = (double)item->data.S32;
            break;
        default:
            psError(PS_ERR_IO, true, "Concept %s (%s) is not of double-precision floating point type (%x) "
                    "--- treating as undefined.\n", name, comment, item->type);
        }
    } else {
        psError(PS_ERR_IO, true, "Concept %s (%s) is not defined.\n", name, comment);
    }
    psTrace(__func__, 7, "Adding %s (%s): %f\n", name, comment, value);

    psMetadataAdd(concepts, PS_LIST_TAIL, name, PS_TYPE_F64 | PS_META_REPLACE, comment, value);
}

void p_pmFPAConceptGetS32(pmFPA *fpa,   // The FPA
                          pmChip *chip, // The chip
                          pmCell *cell, // The cell
                          psDB *db,     // DB handle
                          psMetadata *concepts, // The concepts MD
                          const char *name, // Name of the concept
                          const char *comment // Comment for concept
                         )
{
    psMetadataItem *item = p_pmFPAConceptGet(fpa, chip, cell, db, name);
    int value = 0;
    if (item) {
        switch (item->type) {
        case PS_TYPE_S32:
            value = item->data.S32;
            break;
        case PS_TYPE_F32:
            psLogMsg(__func__, PS_LOG_WARN, "Concept %s (%s) should be S32, but is F32 --- converting.\n",
                     name, comment);
            value = (int)item->data.F32;
            break;
        case PS_TYPE_F64:
            psLogMsg(__func__, PS_LOG_WARN, "Concept %s (%s) should be S32, but is F64 --- converting.\n",
                     name, comment);
            value = (int)item->data.F64;
            break;
        default:
            psError(PS_ERR_IO, true, "Concept %s (%s) is not of integer type (%x) --- treating as "
                    "undefined.\n", name, comment, item->type);
        }
    } else {
        psError(PS_ERR_IO, true, "Concept %s (%s) is not defined.\n", name, comment);
    }
    psTrace(__func__, 7, "Adding %s (%s): %d\n", name, comment, value);

    psMetadataAdd(concepts, PS_LIST_TAIL, name, PS_TYPE_S32 | PS_META_REPLACE, comment, value);
}

void p_pmFPAConceptGetString(pmFPA *fpa, // The FPA
                             pmChip *chip, // The chip
                             pmCell *cell, // The cell
                             psDB *db,  // DB handle
                             psMetadata *concepts, // The concepts MD
                             const char *name, // Name of the concept
                             const char *comment // Comment for concept
                            )
{
    psMetadataItem *item = p_pmFPAConceptGet(fpa, chip, cell, db, name);
    psString value = NULL;
    if (item) {
        switch (item->type) {
        case PS_DATA_STRING:
            value = psMemIncrRefCounter(item->data.V);
            break;
        case PS_DATA_F32:
            psStringAppend(&value, "%f", item->data.F32);
            break;
        case PS_DATA_S32:
            psStringAppend(&value, "%d", item->data.S32);
            break;
        default:
            psError(PS_ERR_IO, true, "Concept %s (%s) is not of string type (%x) --- treating as "
                    "undefined.\n", name, comment, item->type);
        }
    } else {
        psError(PS_ERR_IO, true, "Concept %s (%s) is not defined.\n", name, comment);
        value = psStringCopy("");
    }
    psTrace(__func__, 7, "Adding %s (%s): %s\n", name, comment, value);

    psMetadataAdd(concepts, PS_LIST_TAIL, name, PS_DATA_STRING | PS_META_REPLACE, comment, value);
    psFree(value);
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Public functions
//////////////////////////////////////////////////////////////////////////////////////////////////////////////

// Ingest concepts for the FPA
void pmFPAIngestConcepts(pmFPA *fpa,    // The FPA
                         psDB *db       // DB handle
                        )
{
    if (! fpa->concepts) {
        fpa->concepts = psMetadataAlloc();
    }

    // FPA.NAME
    p_pmFPAConceptGetString(fpa, NULL, NULL, db, fpa->concepts, "FPA.NAME", "Name of FPA");

    // FPA.AIRMASS
    p_pmFPAConceptGetF32(fpa, NULL, NULL, db, fpa->concepts, "FPA.AIRMASS", "Airmass at boresight");

    // FPA.FILTER
    p_pmFPAConceptGetString(fpa, NULL, NULL, db, fpa->concepts, "FPA.FILTER", "Filter used");

    // FPA.POSANGLE
    p_pmFPAConceptGetF32(fpa, NULL, NULL, db, fpa->concepts, "FPA.POSANGLE", "Position angle for instrument");

    // FPA.RADECSYS
    p_pmFPAConceptGetString(fpa, NULL, NULL, db, fpa->concepts, "FPA.RADECSYS", "Celestial coordinate system");

    // These take some extra work

    // FPA.RA
    {
        double ra = NAN;                // The RA
        psMetadataItem *raItem = p_pmFPAConceptGet(fpa, NULL, NULL, db, "FPA.RA"); // The FPA.RA item
        if (raItem)
        {
            switch (raItem->type) {
            case PS_TYPE_F32:
                ra = raItem->data.F32;
                break;
            case PS_TYPE_F64:
                ra = raItem->data.F64;
                break;
            case PS_DATA_STRING:
                // Sexagesimal format
                {
                    int big, medium;
                    float small;
                    // XXX: Upgrade path is to allow dd:mm.mmm
                    if (sscanf(raItem->data.V, "%d:%d:%f", &big, &medium, &small) != 3 &&
                            sscanf(raItem->data.V, "%d %d %f", &big, &medium, &small) != 3)
                    {
                        psError(PS_ERR_IO, true, "Cannot interpret FPA.RA: %s\n", raItem->data.V);
                        break;
                    }
                    ra = abs(big) + (float)medium/60.0 + small/3600.0;
                    if (big < 0)
                    {
                        ra *= -1.0;
                    }
                }
                break;
            default:
                psError(PS_ERR_IO, true, "FPA.RA is of an unexpected type: %x\n", raItem->type);
            }

            // How to interpret the RA
            const psMetadata *camera = fpa->camera; // Camera configuration data
            bool mdok = true;           // Status of MD lookup
            psMetadata *formats = psMetadataLookupMD(&mdok, camera, "FORMATS");
            if (mdok && formats) {
                psString raFormat = psMetadataLookupStr(&mdok, formats, "FPA.RA");
                if (mdok && strlen(raFormat) > 0) {
                    if (strcasecmp(raFormat, "HOURS") == 0) {
                        ra *= M_PI / 12.0;
                    } else if (strcasecmp(raFormat, "DEGREES") == 0) {
                        ra *= M_PI / 180.0;
                    } else if (strcasecmp(raFormat, "RADIANS") == 0) {
                        // No action required
                    } else {
                        psLogMsg(__func__, PS_LOG_WARN, "Don't understand FPA.RA in FORMATS (%s) --- assuming"
                                 " HOURS.\n");
                        ra *= M_PI / 12.0;
                    }
                } else {
                    psError(PS_ERR_IO, false, "Unable to find FPA.RA in FORMATS --- assuming HOURS.\n");
                    ra *= M_PI / 12.0;
                }
            } else {
                psError(PS_ERR_IO, false, "Unable to find FORMAT metadata in camera configuration --- "
                        "assuming format for FPA.RA is HOURS.\n");
                ra *= M_PI / 12.0;
            }
        } else
        {
            psError(PS_ERR_IO, false, "Couldn't find FPA.RA.\n");
        }

        psMetadataAdd(fpa->concepts, PS_LIST_TAIL, "FPA.RA", PS_DATA_F64 | PS_META_REPLACE,
                      "Right Ascension of the boresight (radians)", ra);

    }

    // FPA.DEC
    {
        double dec = NAN;               // The DEC
        psMetadataItem *decItem = p_pmFPAConceptGet(fpa, NULL, NULL, db, "FPA.DEC"); // The FPA.DEC item
        if (decItem) {
            switch (decItem->type) {
            case PS_TYPE_F32:
                dec = decItem->data.F32;
                break;
            case PS_TYPE_F64:
                dec = decItem->data.F64;
                break;
            case PS_DATA_STRING:
                // Sexagesimal format
                {
                    int big, medium;
                    float small;
                    // XXX: Upgrade path is to allow dd:mm.mmm
                    if (sscanf(decItem->data.V, "%d:%d:%f", &big, &medium, &small) != 3 &&
                            sscanf(decItem->data.V, "%d %d %f", &big, &medium, &small) != 3)
                    {
                        psError(PS_ERR_IO, true, "Cannot interpret FPA.DEC: %s\n", decItem->data.V);
                        break;
                    }
                    dec = abs(big) + (float)medium/60.0 + small/3600.0;
                    if (big < 0)
                    {
                        dec *= -1.0;
                    }
                }
                break;
            default:
                psError(PS_ERR_IO, true, "FPA.DEC is of an unexpected type: %x\n", decItem->type);
            }

            // How to interpret the DEC
            const psMetadata *camera = fpa->camera; // Camera configuration data
            bool mdok = true;           // Status of MD lookup
            psMetadata *formats = psMetadataLookupMD(&mdok, camera, "FORMATS");
            if (mdok && formats) {
                psString decFormat = psMetadataLookupStr(&mdok, formats, "FPA.DEC");
                if (mdok && strlen(decFormat) > 0) {
                    if (strcasecmp(decFormat, "HOURS") == 0) {
                        dec *= M_PI / 12.0;
                    } else if (strcasecmp(decFormat, "DEGREES") == 0) {
                        dec *= M_PI / 180.0;
                    } else if (strcasecmp(decFormat, "RADIANS") == 0) {
                        // No action required
                    } else {
                        psLogMsg(__func__, PS_LOG_WARN, "Don't understand FPA.DEC in FORMATS (%s) --- "
                                 "assuming DEGREES.\n");
                        dec *= M_PI / 180.0;
                    }
                } else {
                    psError(PS_ERR_IO, false, "Unable to find FPA.DEC in FORMATS --- assuming DEGREES.\n");
                    dec *= M_PI / 180.0;
                }
            } else {
                psError(PS_ERR_IO, false, "Unable to find FORMATS metadata in camera configuration --- "
                        "assuming format for FPA.DEC is DEGREES.\n");
                dec *= M_PI / 180.0;
            }
        } else {
            psError(PS_ERR_IO, false, "Couldn't find FPA.DEC.\n");
        }

        psMetadataAdd(fpa->concepts, PS_LIST_TAIL, "FPA.DEC", PS_DATA_F64 | PS_META_REPLACE,
                      "Declination of the boresight (radians)", dec);

    }

    // Pau.
}


// Ingest concepts for the chip
bool pmChipIngestConcepts(pmChip *chip, // The chip
                          psDB *db      // DB handle
                         )
{
    //    pmFPA *fpa = chip->parent;          // The parent FPA

    if (! chip->concepts) {
        chip->concepts = psMetadataAlloc();
    }

    // CHIP.NAME --- added by pmFPAConstruct

    // Pau.
    return true;
}


// Add corrective to a position --- in case the user wants FORTRAN indexing (like the FITS standard...)
static bool correctPosition(pmFPA *fpa, // FPA, contains the camera configuration
                            pmCell *cell, // Cell containing the concept to correct
                            const char *conceptName // Name of concept to correct
                           )
{
    bool mdok = false;              // Result of MD lookup
    psMetadata *formats = psMetadataLookupMD(&mdok, fpa->camera, "FORMATS");
    if (mdok && formats) {
        psString format = psMetadataLookupStr(&mdok, formats, conceptName);
        if (mdok && strlen(format) > 0 && strcasecmp(format, "FORTRAN") == 0) {
            psMetadataItem *valueItem = psMetadataLookup(cell->concepts, conceptName);
            valueItem->data.S32 -= 1;
        }
    }
    return true;
}

bool p_pmCellIngestConcept(pmFPA *fpa,  // The FPA
                           pmChip *chip, // The chip
                           pmCell *cell, // The cell
                           psDB *db,    // DB handle
                           const char *concept // Name of the concept
                          )
{
    if (strcmp(concept, "CELL.GAIN") == 0) {
        p_pmFPAConceptGetF32(fpa, chip, cell, db, cell->concepts, "CELL.GAIN", "CCD gain (e/count)");
    } else if (strcmp(concept, "CELL.READNOISE") == 0) {
        p_pmFPAConceptGetF32(fpa, chip, cell, db, cell->concepts, "CELL.READNOISE", "CCD read noise (e)");
    } else if (strcmp(concept, "CELL.SATURATION") == 0) {
        p_pmFPAConceptGetF32(fpa, chip, cell, db, cell->concepts, "CELL.SATURATION",
                             "Saturation level (ADU)");
    } else if (strcmp(concept, "CELL.BAD") == 0) {
        p_pmFPAConceptGetF32(fpa, chip, cell, db, cell->concepts, "CELL.BAD", "Bad level (ADU)");
    } else if (strcmp(concept, "CELL.XPARITY") == 0) {
        p_pmFPAConceptGetS32(fpa, chip, cell, db, cell->concepts, "CELL.XPARITY",
                             "Orientation in x compared to the rest of the FPA");
    } else if (strcmp(concept, "CELL.YPARITY") == 0) {
        p_pmFPAConceptGetS32(fpa, chip, cell, db, cell->concepts, "CELL.YPARITY",
                             "Orientation in y compared to the rest of the FPA");
    } else if (strcmp(concept, "CELL.READDIR") == 0) {
        p_pmFPAConceptGetS32(fpa, chip, cell, db, cell->concepts, "CELL.READDIR",
                             "Read direction: 1=row, 2=col");
    } else if (strcmp(concept, "CELL.EXPOSURE") == 0) { // used to be READOUT.EXPOSURE
        p_pmFPAConceptGetF32(fpa, chip, cell, db, cell->concepts, "CELL.EXPOSURE", "Exposure time (sec)");
    } else if (strcmp(concept, "CELL.DARKTIME") == 0) { // used to be READOUT.DARKTIME
        p_pmFPAConceptGetF32(fpa, chip, cell, db, cell->concepts, "CELL.DARKTIME",
                             "Time since CCD flush (sec)");
        // These take some extra work
    } else if (strcmp(concept, "CELL.TRIMSEC") == 0) {
        psRegion *trimsec = psAlloc(sizeof(psRegion)); // Make space for a psRegion (usually passed by value)

        psMetadataItem *secItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.TRIMSEC");
        if (! secItem) {
            psError(PS_ERR_IO, false, "Couldn't find CELL.TRIMSEC.\n");
            *trimsec = psRegionSet(0.0, 0.0, 0.0, 0.0);
        } else if (secItem->type != PS_DATA_STRING) {
            psError(PS_ERR_IO, true, "CELL.TRIMSEC is not of type STR (%x)\n", secItem->type);
            *trimsec = psRegionSet(0.0, 0.0, 0.0, 0.0);
        } else {
            psString section = secItem->data.V; // The section string

            psMetadataItem *sourceItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.TRIMSEC.SOURCE");
            if (! sourceItem) {
                psError(PS_ERR_IO, false, "Couldn't find CELL.TRIMSEC.SOURCE.\n");
                *trimsec = psRegionSet(0.0, 0.0, 0.0, 0.0);
            } else if (sourceItem->type != PS_DATA_STRING) {
                psError(PS_ERR_IO, true, "CELL.TRIMSEC.SOURCE is not of type STR (%x)\n", sourceItem->type);
                *trimsec = psRegionSet(0.0, 0.0, 0.0, 0.0);
            } else {
                psString source = sourceItem->data.V; // The source string

                if (strcasecmp(source, "VALUE") == 0) {
                    *trimsec = psRegionFromString(section);
                } else if (strcasecmp(source, "HEADER") == 0) {
                    psMetadata *header = NULL; // The FITS header
                    if (cell->hdu) {
                        header = cell->hdu->header;
                    } else if (chip->hdu) {
                        header = chip->hdu->header;
                    } else if (fpa->hdu) {
                        header = fpa->hdu->header;
                    }
                    if (! header) {
                        psError(PS_ERR_IO, true, "Unable to find FITS header!\n");
                        *trimsec = psRegionSet(0.0, 0.0, 0.0, 0.0);
                    } else {
                        bool mdok = true;               // Status of MD lookup
                        psString secValue = psMetadataLookupStr(&mdok, header, section);
                        if (! mdok || ! secValue) {
                            psError(PS_ERR_IO, false, "Unable to locate header %s\n", section);
                            *trimsec = psRegionSet(0.0, 0.0, 0.0, 0.0);
                        } else {
                            *trimsec = psRegionFromString(secValue);
                        }
                    }
                } else {
                    psError(PS_ERR_IO, true, "CELL.TRIMSEC.SOURCE (%s) is not HEADER or VALUE --- trying "
                            "VALUE.\n", source);
                    *trimsec = psRegionFromString(section);
                } // Value of CELL.TRIMSEC.SOURCE
            } // Looking up CELL.TRIMSEC.SOURCE
        } // Looking up CELL.TRIMSEC

        psMetadataAdd(cell->concepts, PS_LIST_TAIL, "CELL.TRIMSEC", PS_DATA_UNKNOWN | PS_META_REPLACE,
                      "Trim section", trimsec);
        psFree(trimsec);

    } else if (strcmp(concept, "CELL.BIASSEC") == 0) {
        psList *biassecs = psListAlloc(NULL); // List of bias sections

        psMetadataItem *secItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.BIASSEC");
        if (! secItem) {
            psError(PS_ERR_IO, false, "Couldn't find CELL.BIASSEC.\n");
        } else if (secItem->type != PS_DATA_STRING) {
            psError(PS_ERR_IO, true, "CELL.BIASSEC is not of type STR (%x)\n", secItem->type);
        } else {
            psString sections = secItem->data.V; // The section string

            psMetadataItem *sourceItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.BIASSEC.SOURCE");
            if (! sourceItem) {
                psError(PS_ERR_IO, false, "Couldn't find CELL.BIASSEC.SOURCE.\n");
            } else if (sourceItem->type != PS_DATA_STRING) {
                psError(PS_ERR_IO, true, "CELL.BIASSEC.SOURCE is not of type STR (%x)\n", sourceItem->type);
            } else {
                psString source = sourceItem->data.V; // The source string

                psList *secList = psStringSplit(sections, " ;"); // List of sections
                psListIterator *secIter = psListIteratorAlloc(secList, PS_LIST_HEAD, false); // Iterator over
                // sections
                psString aSection = NULL; // A section from the list
                while ((aSection = psListGetAndIncrement(secIter))) {
                    psRegion *region = psAlloc(sizeof(psRegion)); // Make space for a psRegion (usually passed
                    // by value)

                    if (strcasecmp(source, "VALUE") == 0) {
                        *region = psRegionFromString(aSection);
                    } else if (strcasecmp(source, "HEADER") == 0) {
                        psMetadata *header = NULL; // The FITS header
                        if (cell->hdu) {
                            header = cell->hdu->header;
                        } else if (chip->hdu) {
                            header = chip->hdu->header;
                        } else if (fpa->hdu) {
                            header = fpa->hdu->header;
                        }
                        if (! header) {
                            psError(PS_ERR_IO, true, "Unable to find FITS header!\n");
                            *region = psRegionSet(0.0,0.0,0.0,0.0);
                        } else {
                            bool mdok = true;           // Status of MD lookup
                            psString secValue = psMetadataLookupStr(&mdok, header, aSection);
                            if (! mdok || ! secValue) {
                                psError(PS_ERR_IO, false, "Unable to locate header %s\n", aSection);
                                *region = psRegionSet(0.0,0.0,0.0,0.0);
                            } else {
                                *region = psRegionFromString(secValue);
                            }
                        }
                    } else {
                        psError(PS_ERR_IO, true, "CELL.BIASSEC.SOURCE (%s) is not HEADER or VALUE --- trying "
                                "VALUE.\n", source);
                        *region = psRegionFromString(aSection);
                    } // Value of CELL.BIASSEC.SOURCE

                    psListAdd(biassecs, PS_LIST_TAIL, region);
                    psFree(region);
                } // Iterating over multiple sections
                psFree(secIter);
                psFree(secList);
            } // Looking up CELL.BIASSEC.SOURCE
        } // Looking up CELL.BIASSEC

        psMetadataAdd(cell->concepts, PS_LIST_TAIL, "CELL.BIASSEC", PS_DATA_LIST | PS_META_REPLACE,
                      "Bias sections", biassecs);
        psFree(biassecs);

    } else if (strcmp(concept, "CELL.XBIN") == 0) {
        int xBin = 1;                   // Binning factor in x
        psMetadataItem *binItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.XBIN");
        if (! binItem) {
            psError(PS_ERR_IO, false, "Couldn't find CELL.XBIN.\n");
        } else if (binItem->type == PS_DATA_STRING) {
            psString binString = binItem->data.V; // The string containing the binning
            if (sscanf(binString, "%d %*d", &xBin) != 1 &&
                    sscanf(binString, "%d,%*d", &xBin) != 1) {
                psError(PS_ERR_IO, true, "Unable to read string to get x binning: %s\n", binString);
            }
        } else if (binItem->type == PS_TYPE_S32) {
            xBin = binItem->data.S32;
        } else {
            psError(PS_ERR_IO, true, "Note sure how to interpret CELL.XBIN of type %x --- assuming 1.\n",
                    binItem->type);
        }

        psMetadataAdd(cell->concepts, PS_LIST_TAIL, "CELL.XBIN", PS_TYPE_S32 | PS_META_REPLACE,
                      "Binning in x", xBin);

    } else if (strcmp(concept, "CELL.YBIN") == 0) {
        int yBin = 1;                   // Binning factor in y
        psMetadataItem *binItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.YBIN");
        if (! binItem) {
            psError(PS_ERR_IO, false, "Couldn't find CELL.YBIN.\n");
        } else if (binItem->type == PS_DATA_STRING) {
            psString binString = binItem->data.V; // The string containing the binning
            if (sscanf(binString, "%*d %d", &yBin) != 1 &&
                    sscanf(binString, "%*d,%d", &yBin) != 1) {
                psError(PS_ERR_IO, true, "Unable to read string to get y binning: %s\n", binString);
            }
        } else if (binItem->type == PS_TYPE_S32) {
            yBin = binItem->data.S32;
        } else {
            psError(PS_ERR_IO, true, "Note sure how to interpret CELL.YBIN of type %x --- assuming 1.\n",
                    binItem->type);
        }

        psMetadataAdd(cell->concepts, PS_LIST_TAIL, "CELL.YBIN", PS_TYPE_S32 | PS_META_REPLACE,
                      "Binning in y", yBin);

    } else if (strcmp(concept, "CELL.TIME") == 0 || strcmp(concept, "CELL.TIMESYS") == 0) {
        // Do CELL.TIME and CELL.TIMESYS together
        psTime *time = NULL;            // The time
        psTimeType timeSys = PS_TIME_UTC; // The time system

        // CELL.TIMESYS
        psMetadataItem *sysItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.TIMESYS");
        if (! sysItem) {
            psError(PS_ERR_IO, true, "Couldn't find CELL.TIMESYS --- assuming UTC.\n");
        } else if (sysItem->type != PS_DATA_STRING) {
            psError(PS_ERR_IO, true, "CELL.TIMESYS isn't of type STRING --- assuming UTC.\n");
        } else {
            psString sys = sysItem->data.V; // The time system string
            if (strcasecmp(sys, "TAI") == 0) {
                timeSys = PS_TIME_TAI;
            } else if (strcasecmp(sys, "UTC") == 0) {
                timeSys = PS_TIME_UTC;
            } else if (strcasecmp(sys, "UT1") == 0) {
                timeSys = PS_TIME_UT1;
            } else if (strcasecmp(sys, "TT") == 0) {
                timeSys = PS_TIME_TT;
            } else {
                psError(PS_ERR_IO, true, "Can't interpret CELL.TIMESYS --- assuming UTC.\n");
            }
        }
        psMetadataAdd(cell->concepts, PS_LIST_TAIL, "CELL.TIMESYS", PS_TYPE_S32 | PS_META_REPLACE,
                      "Time system", timeSys);

        psMetadataItem *timeItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.TIME");
        if (! timeItem) {
            psError(PS_ERR_IO, false, "Couldn't find CELL.TIME.\n");
        } else {
            // Get format
            const psMetadata *camera = fpa->camera; // The camera configuration data
            bool mdok = true;           // Status of MD lookup
            psMetadata *formats = psMetadataLookupMD(&mdok, camera, "FORMATS");
            if (mdok && formats) {
                psString timeFormat = psMetadataLookupStr(&mdok, formats, "CELL.TIME");
                if (mdok && strlen(timeFormat) > 0) {
                    switch (timeItem->type) {
                    case PS_DATA_STRING: {
                            psString timeString = timeItem->data.V;     // String with the time
                            if (strcasecmp(timeFormat, "ISO") == 0) {
                                // timeString contains an ISO time
                                time = psTimeFromISO(timeString, timeSys);
                            } else if (strcasecmp(timeFormat, "JD") == 0) {
                                double timeValue = strtod (timeString, NULL);
                                time = psTimeFromJD(timeValue);
                            } else if (strcasecmp(timeFormat, "MJD") == 0) {
                                double timeValue = strtod (timeString, NULL);
                                time = psTimeFromMJD(timeValue);
                            } else if (strstr(timeFormat, "SEPARATE")) {
                                // timeString contains headers for the date and time
                                psMetadata *header = NULL; // The FITS header
                                if (cell->hdu) {
                                    header = cell->hdu->header;
                                } else if (chip->hdu) {
                                    header = chip->hdu->header;
                                } else if (fpa->hdu) {
                                    header = fpa->hdu->header;
                                }
                                if (! header) {
                                    psError(PS_ERR_IO, true, "Unable to find FITS header!\n");
                                } else {
                                    // Get the headers
                                    char *stuff1 = strpbrk(timeString, " ,;");
                                    psString dateName = psStringNCopy(timeString,
                                                                      strlen(timeString) - strlen(stuff1));
                                    char *stuff2 = strpbrk(stuff1, "abcdefghijklmnopqrstuvwxyz"
                                                           "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
                                    psString timeName = psStringCopy(stuff2);

                                    bool mdok = true; // Status of MD lookup
                                    psString dateString = psMetadataLookupStr(&mdok, header, dateName);
                                    psFree(dateName);
                                    int day = 0, month = 0, year = 0;
                                    if (sscanf(dateString, "%d-%d-%d", &day, &month, &year) != 3 &&
                                            sscanf(dateString, "%d/%d/%d", &day, &month, &year) != 3) {
                                        psError(PS_ERR_IO, true, "Unable to read date: %s\n", dateString);
                                    } else {
                                        if (strstr(timeFormat, "BACKWARDS")) {
                                            int temp = day;
                                            day = year;
                                            year = temp;
                                        }
                                        if (strstr(timeFormat, "PRE2000") || year < 2000) {
                                            year += 2000;
                                        }

                                        psMetadataItem *timeItem = psMetadataLookup(header, timeName);
                                        if (! timeItem) {
                                            psError(PS_ERR_IO, false, "Unable to find time header: %s\n",
                                                    timeName);
                                        } else if (timeItem->type == PS_DATA_STRING) {
                                            // Time is a string, in the usual way:
                                            psStringAppend(&dateString, "T%s", timeItem->data.V);
                                        } else {
                                            // Assume that time is specified in Second of Day
                                            double seconds = NAN;
                                            switch (timeItem->type) {
                                            case PS_TYPE_S32:
                                                seconds = timeItem->data.S32;
                                                break;
                                            case PS_TYPE_F32:
                                                seconds = timeItem->data.F32;
                                                break;
                                            case PS_TYPE_F64:
                                                seconds = timeItem->data.F64;
                                                break;
                                            default:
                                                psError(PS_ERR_IO, true, "Time header (%s) is not of an "
                                                        "expected type: %x\n", timeName, timeItem->type);
                                            }
                                            // Now print to timeString as "hh:mm:ss.ss"
                                            int hours = seconds / 3600;
                                            seconds -= (double)hours * 3600.0;
                                            int minutes = seconds / 60;
                                            seconds -= (double)minutes * 60.0;
                                            psStringAppend(&dateString, "T%02d:%02d:%02f", hours, minutes,
                                                           seconds);
                                        }
                                        time = psTimeFromISO(dateString, timeSys);
                                    } // Reading date and time
                                    psFree(timeName);
                                } // Reading headers
                            } else {
                                psError(PS_ERR_IO, true, "Not sure how to parse CELL.TIME (%s) --- trying "
                                        "ISO\n", timeString);
                                time = psTimeFromISO(timeString, timeSys);
                            } // Interpreting the time string
                        }
                        break;
                    case PS_TYPE_F32: {
                            double timeValue = (double)timeItem->data.F32;
                            if (strcasecmp(timeFormat, "JD") == 0) {
                                time = psTimeFromJD(timeValue);
                            } else if (strcasecmp(timeFormat, "MJD") == 0) {
                                time = psTimeFromMJD(timeValue);
                            } else {
                                psError(PS_ERR_IO, true, "Not sure how to parse CELL.TIME (%f) --- trying "
                                        "JD\n", timeValue);
                                time = psTimeFromJD(timeValue);
                            }
                        }
                        break;
                    case PS_TYPE_F64: {
                            double timeValue = (double)timeItem->data.F64;
                            if (strcasecmp(timeFormat, "JD") == 0) {
                                time = psTimeFromJD(timeValue);
                            } else if (strcasecmp(timeFormat, "MJD") == 0) {
                                time = psTimeFromMJD(timeValue);
                            } else {
                                psError(PS_ERR_IO, true, "Not sure how to parse CELL.TIME (%f) --- trying "
                                        "JD\n", timeValue);
                                time = psTimeFromJD(timeValue);
                            }
                        }
                        break;
                    default:
                        psError(PS_ERR_IO, true, "Unable to parse CELL.TIME.\n");
                    }
                } else {
                    psError(PS_ERR_IO, false, "Unable to find CELL.TIME in FORMATS.\n");
                } // Getting the format
            } else {
                psError(PS_ERR_IO, false, "Unable to find FORMATS in camera configuration.\n");
            } // Getting the formats
        } // Getting CELL.TIME

        psMetadataAdd(cell->concepts, PS_LIST_TAIL, "CELL.TIME", PS_DATA_TIME | PS_META_REPLACE,
                      "Time of exposure", time);
        psFree(time);

        // These are new and experimental concepts: CELL.X0 and CELL.Y0
    } else if (strcmp(concept, "CELL.X0") == 0) {
        p_pmFPAConceptGetS32(fpa, chip, cell, db, cell->concepts, "CELL.X0", "Position of (0,0) on the chip");
        correctPosition(fpa, cell, "CELL.X0");
    } else if (strcmp(concept, "CELL.Y0") == 0) {
        p_pmFPAConceptGetS32(fpa, chip, cell, db, cell->concepts, "CELL.Y0", "Position of (0,0) on the chip");
        correctPosition(fpa, cell, "CELL.Y0");
    }

    return true;
}



// Ingest concepts for the cell
bool pmCellIngestConcepts(pmCell *cell, // The cell
                          psDB *db      // DB handle
                         )
{
    pmChip *chip = cell->parent;        // The parent chip
    pmFPA *fpa = chip->parent;          // The parent FPA

    if (! cell->concepts) {
        cell->concepts = psMetadataAlloc();
    }

    // CELL.NAME --- added by pmFPAConstruct

    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.GAIN");
    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.READNOISE");
    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.SATURATION");
    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.BAD");
    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.XPARITY");
    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.YPARITY");
    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.READDIR");

    // These used to be pmReadoutGetExposure and pmReadoutGetDarkTime, 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....
    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.EXPOSURE");
    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.DARKTIME");


    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.TRIMSEC");
    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.BIASSEC");
    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.TIME"); // This also does CELL.TIMESYS
    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.XBIN");
    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.YBIN");
    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.X0");
    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.Y0");

    return true;
}


// Retrieve a concept
psMetadataItem *pmCellGetConcept(pmCell *cell, // The cell
                                 const char *concept // The concept
                                )
{
    psMetadataItem *item = psMetadataLookup(cell->concepts, concept);
    if (! item) {
        pmChip *chip = cell->parent;
        item = psMetadataLookup(chip->concepts, concept);
        if (! item) {
            pmFPA *fpa = chip->parent;
            item = psMetadataLookup(fpa->concepts, concept);
        }
    }
    return item;                        // item is either NULL or is what we want.
}


// Set up concepts for blank image
bool pmFPABlankConcepts(pmFPA *fpa      // The FPA
                       )
{
    assert(fpa);

    // FPA-level concepts
    if (! fpa->concepts) {
        fpa->concepts = psMetadataAlloc();
    }
    psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.NAME", PS_META_REPLACE, "Name of FPA", "");
    psMetadataAddF32(fpa->concepts, PS_LIST_TAIL, "FPA.AIRMASS", PS_META_REPLACE,
                     "Airmass at boresight", NAN);
    psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.FILTER", PS_META_REPLACE, "Filter used", "");
    psMetadataAddF32(fpa->concepts, PS_LIST_TAIL, "FPA.POSANGLE", PS_META_REPLACE,
                     "Position angle for instrument", NAN);
    psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.RADECSYS", PS_META_REPLACE,
                     "Celestial coordinate system", "");
    psMetadataAddF64(fpa->concepts, PS_LIST_TAIL, "FPA.RA", PS_META_REPLACE,
                     "Right Ascension of the boresight (radians)", NAN);
    psMetadataAddF64(fpa->concepts, PS_LIST_TAIL, "FPA.DEC", PS_META_REPLACE,
                     "Declination of the boresight (radians)", NAN);

    // Iterate through the chips
    psArray *chips = fpa->chips;        // Array of chips
    for (int i = 0; i < chips->n; i++) {
        pmChip *chip = chips->data[i];  // The chip of interest
        if (! chip) {
            continue;
        }

        // Chip-level concepts
        if (! chip->concepts) {
            chip->concepts = psMetadataAlloc();
        }
        // CHIP.NAME *should* have been added by pmFPAConstruct --- make sure
        psMetadataItem *chipNameItem = psMetadataLookup(chip->concepts, "CHIP.NAME");
        if (! chipNameItem) {
            psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "CHIP.NAME", PS_META_REPLACE, "Name of Chip", "");
        }

        // Iterate through the cells
        psArray *cells = chip->cells;   // Array of cells
        for (int j = 0; j < cells->n; j++) {
            pmCell *cell = cells->data[i]; // The cell of interest
            if (! cell) {
                continue;
            }

            // Cell-level concepts
            if (! cell->concepts) {
                cell->concepts = psMetadataAlloc();
            }
            // CELL.NAME *should* have been added by pmFPAConstruct --- make sure
            psMetadataItem *cellNameItem = psMetadataLookup(cell->concepts, "CELL.NAME");
            if (!cellNameItem) {
                psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "CELL.NAME", PS_META_REPLACE,
                                 "Name of Cell", "");
            }
            psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.GAIN", PS_META_REPLACE, "CCD gain (e/count)", NAN);
            psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.READNOISE", PS_META_REPLACE, "CCD read noise (e)", NAN);
            psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.SATURATION", PS_META_REPLACE, "Saturation level (ADU)", NAN);
            psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.BAD", PS_META_REPLACE, "Bad level (ADU)", NAN);
            psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.XPARITY", PS_META_REPLACE, "Orientation in x compared to the rest of the FPA", NAN);
            psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.YPARITY", PS_META_REPLACE, "Orientation in x compared to the rest of the FPA", NAN);
            psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.READDIR", PS_META_REPLACE, "Read direction: 1=row, 2=col", NAN);
            psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.EXPOSURE", PS_META_REPLACE, "Exposure time (sec)", NAN);
            psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.DARKTIME", PS_META_REPLACE, "Time since CCD flush (sec)", NAN);
            psMetadataAddS32(cell->concepts, PS_LIST_TAIL, "CELL.XBIN", PS_META_REPLACE, "Binning in x", 0);
            psMetadataAddS32(cell->concepts, PS_LIST_TAIL, "CELL.YBIN", PS_META_REPLACE, "Binning in y", 0);
            psMetadataAddS32(cell->concepts, PS_LIST_TAIL, "CELL.X0", PS_META_REPLACE, "Position of (0,0) on the chip", 0);
            psMetadataAddS32(cell->concepts, PS_LIST_TAIL, "CELL.Y0", PS_META_REPLACE, "Position of (0,0) on the chip", 0);

            psRegion *trimsec = psAlloc(sizeof(psRegion)); // Dummy region
            trimsec->x0 = NAN;
            trimsec->x1 = NAN;
            trimsec->y0 = NAN;
            trimsec->y1 = NAN;
            psMetadataAddPtr(cell->concepts, PS_LIST_TAIL, "CELL.TRIMSEC", PS_META_REPLACE, "Trim section", trimsec);
            psList *biassecs = psListAlloc(NULL); // Dummy list
            psMetadataAddPtr(cell->concepts, PS_LIST_TAIL, "CELL.BIASSEC", PS_META_REPLACE, "Bias section", biassecs);
            psTime *time = psTimeAlloc(PS_TIME_TAI); // Dummy time
            psMetadataAddPtr(cell->concepts, PS_LIST_TAIL, "CELL.TIME", PS_META_REPLACE, "Time of exposure", time);
            psMetadataAddS32(cell->concepts, PS_LIST_TAIL, "CELL.TIMESYS", PS_META_REPLACE, "Time system", PS_TIME_TAI);
        }
    }

    return true;
}

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

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

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

    return true;
}
