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

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

#include "pmFPA.h"
#include "pmFPALevel.h"
#include "pmFPAFlags.h"
#include "pmConcepts.h"
#include "pmFPAConstruct.h"
#include "pmFPAview.h"
#include "pmFPAUtils.h"
#include "pmHDUUtils.h"

//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// File-static functions
//////////////////////////////////////////////////////////////////////////////////////////////////////////////

// Read data for a particular cell from the camera format description
static psMetadata *getCellData(const psMetadata *format, // The camera format description
                               const char *cellName // The name of the cell
                              )
{
    assert(format);
    assert(cellName && strlen(cellName) > 0);

    bool status = true;                 // Result of MD lookup
    psMetadata *cells = psMetadataLookupMetadata(&status, format, "CELLS"); // The CELLS
    if (!status || !cells) {
        psError(PS_ERR_IO, true, "Unable to determine CELLS of camera.\n");
        return NULL;
    }

    psMetadata *cellData = psMetadataLookupMetadata(&status, cells, cellName); // The data for the particular cell
    if (!status || !cellData) {
        psLogMsg(__func__, PS_LOG_WARN, "Unable to find specs for cell %s: ignored\n", cellName);
    }

    return cellData;
}

// Parse a list of first:second:third pairs in a string
static bool parseContent(psArray **first, // Array of the first values
                         psArray **second, // Array of the second values
                         psArray **third, // Array of the third values
                         const char *string // The string to parse
                        )
{
    assert(first);
    assert(second);
    assert(third);
    assert(string && strlen(string) > 0);

    bool allOK = true;                  // Everything was OK?
    psList *values = psStringSplit(string, " ,;", true); // List of the parts
    *first = psArrayAlloc(values->n);
    *second = psArrayAlloc(values->n);
    *third = psArrayAlloc(values->n);
    int num = 0;
    psListIterator *valuesIter = psListIteratorAlloc(values, PS_LIST_HEAD, false); // Iterator for values
    psString value = NULL;               // "first:second:third" string
    while ((value = psListGetAndIncrement(valuesIter))) {
        psList *firstSecondThird = psStringSplit(value, ":", true); // List containing the first, second, third
        psArray *fst = psListToArray(firstSecondThird); // An array representation
        psFree(firstSecondThird);
        psString firstPart = NULL;      // The first part
        psString secondPart = NULL;     // The second part
        psString thirdPart = NULL;      // The third part
        switch (firstSecondThird->n) {
        case 1:
            thirdPart = fst->data[0];
            break;
        case 2:
            secondPart = fst->data[0];
            thirdPart = fst->data[1];
            break;
        case 3:
            firstPart = fst->data[0];
            secondPart = fst->data[1];
            thirdPart = fst->data[2];
            break;
        default:
            psLogMsg(__func__, PS_LOG_WARN, "Badly formated specifier: %s --- ignored.\n", value);
            allOK = false;
            psFree(fst);
            continue;
        }
        psArraySet(*first, num, firstPart);
        psArraySet(*second, num, secondPart);
        psArraySet(*third, num, thirdPart);
        num++;
        psFree(fst);
    }
    psFree(valuesIter);
    psFree(values);

    return allOK;
}

// Get the name of a PHU chip or cell from the header
static psString phuNameFromHeader(const char *name, // The name to lookup: "CELL.NAME" or "CHIP.NAME"
                                  const psMetadata *fileInfo, // FILE within the camera format description
                                  const psMetadata *header // Primary header
                                 )
{
    assert(name && strlen(name) > 0);
    assert(fileInfo);
    assert(header);

    bool mdok = true;                   // Result of MD lookup
    psString keyword = psMetadataLookupStr(&mdok, fileInfo, name);
    if (!mdok || strlen(keyword) == 0) {
        return false;
    }
    psMetadataItem *resultItem = psMetadataLookup(header, keyword);
    if (!resultItem) {
        psError(PS_ERR_IO, true, "Unable to find %s in primary header to identify %s.\n", keyword, name);
        return NULL;
    }
    return psMetadataItemParseString(resultItem);
}

// Add an HDU to the FPA
static bool addHDUtoFPA(pmFPA *fpa,     // FPA to which to add
                        pmHDU *hdu      // HDU to be added
                       )
{
    assert(fpa);
    assert(hdu);

    if (fpa->hdu) {
        // Something's already here
        if (fpa->hdu != hdu) {
            psError(PS_ERR_IO, true, "Unable to add HDU since FPA already has one.\n");
        }
        return false;
    }
    fpa->hdu = psMemIncrRefCounter(hdu);
    bool success = true;                // Success of reading concepts
    if (hdu->header && !pmConceptsReadFPA(fpa, PM_CONCEPT_SOURCE_DEFAULTS | PM_CONCEPT_SOURCE_HEADER,
                                          false, NULL)) {
        psError(PS_ERR_UNKNOWN, false, "Unable to read concepts from header.\n");
        success = false;
    }
    pmFPASetFileStatus(fpa, true);

    return success;
}

// Add an HDU to the chip
static bool addHDUtoChip(pmChip *chip,  // Chip to which to add
                         pmHDU *hdu     // HDU to be added
                        )
{
    assert(chip);
    assert(hdu);

    if (chip->hdu) {
        // Something's already here
        if (chip->hdu != hdu) {
            psError(PS_ERR_IO, true, "Unable to add HDU since chip already has one.\n");
        }
        return false;
    }
    chip->hdu = psMemIncrRefCounter(hdu);
    bool success = true;                // Success of reading concepts
    if (hdu->header && !pmConceptsReadChip(chip, PM_CONCEPT_SOURCE_DEFAULTS | PM_CONCEPT_SOURCE_HEADER,
                                           true, false, NULL)) {
        psError(PS_ERR_UNKNOWN, false, "Unable to read concepts from header.\n");
        success = false;
    }
    pmChipSetFileStatus(chip, true);

    return success;
}

// Add an HDU to the cell
static bool addHDUtoCell(pmCell *cell,  // Cell to which to add
                         pmHDU *hdu     // HDU to be added
                        )
{
    assert(cell);
    assert(hdu);

    if (cell->hdu) {
        // Something's already here
        if (cell->hdu != hdu) {
            psError(PS_ERR_IO, true, "Unable to add HDU since cell already has one.\n");
        }
        return false;
    }
    cell->hdu = psMemIncrRefCounter(hdu);
    bool success = true;                // Success of reading concepts
    if (hdu->header && !pmConceptsReadCell(cell, PM_CONCEPT_SOURCE_DEFAULTS | PM_CONCEPT_SOURCE_HEADER,
                                           true, NULL)) {
        psError(PS_ERR_UNKNOWN, false, "Unable to read concepts from header.\n");
        success = false;
    }
    pmCellSetFileStatus(cell, true);

    return success;
}


// Looks up the particular content, based on the chip and cell
static const char *getContent(const psMetadata *fileInfo, // The FILE from the camera format configuration
                              const psMetadata *contents, // The CONTENTS from the camera format configuration
                              const pmChip *chip, // The chip of interest
                              const pmCell *cell // The cell of interest
                             )
{
    assert(fileInfo);
    assert(contents);
    assert(chip);

    bool mdok = true;                   // Status of MD lookup
    char *contentKey = psMetadataLookupStr(&mdok, fileInfo, "CONTENT"); // Key for CONTENTS
    if (!mdok || !contentKey || strlen(contentKey) == 0) {
        psError(PS_ERR_IO, true, "Unable to find CONTENT in FILE within camera format configuration.\n");
        return NULL;
    }

    contentKey = psStringCopy(contentKey); // So that we're not altering something someone else might use...

    // Replace some concept names
    if (strstr(contentKey, "{CHIP.NAME}")) {
        if (!chip) {
            psError(PS_ERR_IO, true, "CONTENT in FILE refers to CHIP.NAME, but no chip was provided.\n");
            psFree(contentKey);
            return NULL;
        }
        const char *name = psMetadataLookupStr(&mdok, chip->concepts, "CHIP.NAME");
        if (mdok && name && strlen(name) > 0) {
            psStringSubstitute(&contentKey, name, "{CHIP.NAME}");
        }
    }

    if (strstr(contentKey, "{CELL.NAME}")) {
        if (!chip) {
            psError(PS_ERR_IO, true, "CONTENT in FILE refers to CELL.NAME, but no chip was provided.\n");
            psFree(contentKey);
            return NULL;
        }
        const char *name = psMetadataLookupStr(&mdok, cell->concepts, "CELL.NAME");
        if (mdok && name && strlen(name) > 0) {
            psStringSubstitute(&contentKey, name, "{CELL.NAME}");
        }
    }

    // XXX: MORE SUBSTITUTION OPTIONS HERE!

    psTrace("psModules.camera", 5, "Looking up %s in the CONTENTS.\n", contentKey);
    const char *content = psMetadataLookupStr(&mdok, contents, contentKey);
    if (!mdok || !content || strlen(content) == 0) {
        psFree(contentKey);
        psError(PS_ERR_IO, true, "Unable to find %s in the CONTENTS.\n", contentKey);
        return NULL;
    }

    psFree(contentKey);

    return content;
}


// Given a (string) list of contents "chip:cell:type chip:cell:type", put the HDU in the correct place and
// plug in the cell configuration information
static int processContents(pmFPA *fpa,  // The FPA
                           pmChip *chip, // The chip, or NULL
                           pmCell *cell, // The cell, or NULL
                           pmHDU *hdu,  // The HDU to be added
                           pmFPALevel level, // The level at which to add the HDU
                           const char *contents, // The contents line, consisting of a list of chip:cell
                           const psMetadata *format // Camera format configuration
                          )
{
    assert(fpa);
    assert(contents && strlen(contents) > 0);
    assert(!cell || (cell && chip));    // Need both chip and cell if given a cell
    assert(format);

    if (hdu && level == PM_FPA_LEVEL_FPA) {
        if (!addHDUtoFPA(fpa, hdu)) {
            psError(PS_ERR_UNKNOWN, false, "Adding HDU to FPA (%s)", contents);
            return -1;
        }
    }

    // Parse the list of chip:cell:type
    psArray *chips = NULL;              // The chips (first bits)
    psArray *cells = NULL;              // The cells (second bits)
    psArray *types = NULL;              // The cell types (third bits)
    int numCells = 0;                   // Number of cells processed
    parseContent(&chips, &cells, &types, contents);
    for (int i = 0; i < types->n; i++) {
        psString chipName = chips->data[i]; // The name of the chip
        psString cellName = cells->data[i]; // The name of the cell
        psString cellType = types->data[i]; // The type of the cell

        // Get the chip
        pmChip *newChip = NULL;         // The chip specified
        if (chip) {
            newChip = chip;
        } else if (chipName) {
            // Find the chip
            int chipNum = pmFPAFindChip(fpa, chipName); // The chip we're looking for
            if (chipNum == -1) {
                psLogMsg(__func__, PS_LOG_WARN, "Unable to find chip %s in fpa --- ignored.\n", chipName);
                continue;
            }
            newChip = fpa->chips->data[chipNum];
        }

        if (!newChip) {
            psLogMsg(__func__, PS_LOG_WARN, "Unable to determine chip for entry %d of content (follows) --- "
                     "ignored.\n\t%s", i, contents);
            continue;
        }

        // Put in the HDU
        if (hdu && level == PM_FPA_LEVEL_CHIP) {
            addHDUtoChip(newChip, hdu);
        }
        // load chip-related concepts
        if (!pmConceptsReadChip(newChip, PM_CONCEPT_SOURCE_DEFAULTS, false, false, NULL)) {
            psError(PS_ERR_UNKNOWN, false, "Unable to read concepts from camera and defaults for chip %s\n",
                    chipName);
            return -1;
        }

        // Get the cell
        pmCell *newCell = NULL;         // The cell specified
        if (cell) {
            newCell = cell;
        } else if (cellName) {
            // Find the cell
            int cellNum = pmChipFindCell(newChip, cellName); // The cell we're looking for
            if (cellNum == -1) {
                psLogMsg(__func__, PS_LOG_WARN, "Unable to find cell %s in chip %s --- ignored.\n",
                         cellName, chipName);
                continue;
            }
            newCell = newChip->cells->data[cellNum];
        }
        if (!newCell) {
            psLogMsg(__func__, PS_LOG_WARN, "Unable to determine cell for entry %d of content (follows) --- "
                     "ignored.\n\t%s", i, contents);
            continue;
        }

        // Get the type
        if (!cellType) {
            psLogMsg(__func__, PS_LOG_WARN, "Unable to determine cell type for entry %d of content (follows) "
                     "--- ignored.\n\t%s", i, contents);
            continue;
        }
        psMetadata *cellData = getCellData(format, cellType); // Data for this cell

        if (hdu && level == PM_FPA_LEVEL_CELL) {
            addHDUtoCell(newCell, hdu);
        }

        // Put in the cell data
        if (newCell->config) {
            psLogMsg(__func__, PS_LOG_WARN, "Overwriting cell data in chip %s, cell %s\n", chipName,
                     cellName);
            psFree(newCell->config); // Make way!
        }
        newCell->config = psMemIncrRefCounter(cellData);
        if (!pmConceptsReadCell(newCell, PM_CONCEPT_SOURCE_CELLS | PM_CONCEPT_SOURCE_DEFAULTS,
                                false, NULL)) {
            psError(PS_ERR_UNKNOWN, false, "Unable to read concepts from camera and defaults for chip %s, "
                    "cell %s, type %s", chipName, cellName, cellType);
            return -1;
        }
        numCells++;
    }
    psFree(chips);
    psFree(cells);
    psFree(types);

    return numCells;
}

// Return the level at which EXTENSIONS go, from the FILE metadata within the camera format
static pmFPALevel hduLevel(const psMetadata *format // The camera format configuration
                          )
{
    assert(format);

    bool mdok = true;                   // Status of MD lookup
    psMetadata *file = psMetadataLookupMetadata(&mdok, format, "FILE"); // File information
    if (!mdok || !file) {
        psError(PS_ERR_IO, true, "Unable to find FILE information in camera format configuration.\n");
        return PM_FPA_LEVEL_NONE;
    }
    const char *extType = psMetadataLookupStr(&mdok, file, "EXTENSIONS");
    if (!mdok || !extType || strlen(extType) == 0) {
        psError(PS_ERR_IO, true, "Unable to find EXTENSIONS in the FILE information in the camera format"
                " configuration.\n");
        return PM_FPA_LEVEL_NONE;
    }

    // Where do we stick in the HDUs?
    pmFPALevel level = PM_FPA_LEVEL_NONE; // Level for HDU insertion
    if (strcasecmp(extType, "CHIP") == 0) {
        level = PM_FPA_LEVEL_CHIP;
    } else if (strcasecmp(extType, "CELL") == 0) {
        level = PM_FPA_LEVEL_CELL;
    } else if (strcasecmp(extType, "NONE") != 0) {
        psError(PS_ERR_IO, true, "EXTENSIONS is not CHIP or CELL or NONE.\n");
    }

    return level;
}

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

pmFPA *pmFPAConstruct(const psMetadata *camera)
{
    PS_ASSERT_PTR_NON_NULL(camera, NULL);

    pmFPA *fpa = pmFPAAlloc(camera);    // The FPA to fill out

    bool mdok = true;                   // Status from MD lookups
    psMetadata *components = psMetadataLookupMetadata(&mdok, camera, "FPA"); // FPA components
    if (!mdok || !components) {
        psError(PS_ERR_IO, true, "Failed to lookup \"FPA\"");
        psFree(fpa);
        return NULL;
    }
    psMetadataIterator *componentsIter = psMetadataIteratorAlloc(components, PS_LIST_HEAD, NULL);
    psMetadataItem *componentsItem = NULL; // Item from components
    while ((componentsItem = psMetadataGetAndIncrement(componentsIter))) {
        const char *chipName = componentsItem->name; // Name of the chip
        if (componentsItem->type != PS_DATA_STRING) {
            psLogMsg(__func__, PS_LOG_WARN, "Element %s in FPA within the camera configuration is not of "
                     "type STR (type=%x) --- ignored.\n", chipName, componentsItem->type);
            continue;
        }

        pmChip *chip = pmChipAlloc(fpa, chipName); // The chip
        psList *cellNames = psStringSplit(componentsItem->data.V, " ,;", true); // List of cell names
        psListIterator *cellNamesIter = psListIteratorAlloc(cellNames, PS_LIST_HEAD, false); // Iterator

        psString cellName = NULL;       // Name of cell
        while ((cellName = psListGetAndIncrement(cellNamesIter))) {
            pmCell *cell = pmCellAlloc(chip, cellName); // New cell
            psFree(cell);               // Drop reference
        }
        psFree(chip);                   // Drop reference
        psFree(cellNamesIter);
        psFree(cellNames);
    }
    psFree(componentsIter);

    return fpa;
}


// This is the engine for the pmFPAAddSourceFrom{Header,View} functions.
// It uses the camera format configuration information to determine where HDUs go in the FPA.
// It returns a view corresponding to the PHU
static pmFPAview *addSource(pmFPA *fpa,       // The FPA
                            const char *fpaname, // The desired FPA name
                            const pmFPAview *phuView, // The view corresponding to the PHU, or NULL
                            const psMetadata *header, // The PHU header, or NULL
                            const psMetadata *format, // Format of file
                            bool install // Install the provided header in the location that we find?
                           )
{
    assert(fpa);
    assert(phuView || header);
    assert(format);

    bool mdok;                          // Status of MD lookup

    // If FPA.NAME is already defined, new name must match it; otherwise, warn the user that something
    // potentially bad is happening.
    if (fpaname && install) {
        const char *currentName = psMetadataLookupStr(&mdok, fpa->concepts, "FPA.NAME"); // Current name
        if (mdok && currentName && strlen(currentName) > 0 && strcmp(currentName, fpaname) != 0) {
            psLogMsg(__func__, PS_LOG_WARN, "FPA.NAME for new source (%s) doesn't match FPA.NAME for current "
                     "fpa (%s).\n", fpaname, currentName);
        }
        psMetadataAddStr(fpa->concepts, PS_LIST_HEAD, "FPA.NAME", PS_META_REPLACE, "Name of FPA", fpaname);
    } else if (!psMetadataLookup(fpa->concepts, "FPA.NAME")) {
        // Make sure there is an FPA.NAME
        psMetadataAddStr(fpa->concepts, PS_LIST_HEAD, "FPA.NAME", 0, "Name of FPA", "UNKNOWN");
    }

    psMetadata *fileInfo = psMetadataLookupMetadata(&mdok, format, "FILE"); // The file information
    if (!mdok || !fileInfo) {
        psError(PS_ERR_IO, false, "Unable to find FILE in the camera format configuration.\n");
        return NULL;
    }

    // At what level does the PHU go?
    const char *phuType = psMetadataLookupStr(&mdok, fileInfo, "PHU"); // What is the PHU?
    if (!mdok || strlen(phuType) == 0) {
        psError(PS_ERR_IO, false, "Unable to find PHU in the format specification.\n");
        return NULL;
    }

    // Prepare the PHU to be placed in the camera hierarchy
    pmHDU *phdu = pmHDUAlloc(NULL);     // The primary header data unit
    // Casting to psPtr to avoide "warning: passing arg 1 of `p_psMemIncrRefCounter' discards qualifiers from
    // pointer target type"
    phdu->header = psMemIncrRefCounter((const psPtr)header);
    phdu->format = psMemIncrRefCounter((const psPtr)format);
    pmFPAview *view = pmFPAviewAlloc(0); // View, to be returned
    if (phuView) {
        // Copy the view, for the case where we're given a header.
        *view = *phuView;
    }

    // And at what level do the individual extensions go?
    const char *extType = psMetadataLookupStr(&mdok, fileInfo, "EXTENSIONS"); // What's in the extns?
    if (!mdok || strlen(extType) == 0) {
        psError(PS_ERR_IO, false, "Unable to find EXTENSIONS in the format specification.\n");
        psFree(view);
        return NULL;
    }

    // Now, there are a few cases:

    // 1. PHU=FPA and EXTENSIONS=NONE.  This is a single CCD imager, where the entire FPA is in the PHU image.
    // In this case, the CONTENTS is of type STR (rather than METADATA), and has the usual chip:cell:cellType.
    //
    // 2. PHU=CHIP or PHU=CELL, and EXTENSIONS=NONE.  This is a single chip or cell from a mosaic camera in
    // its own file (e.g., Megacam split).  In this case, the CONTENTS is of type METADATA, and consists of a
    // menu of choices for the contents of the file.  This is because we need to work out which chip and/or
    // cell it comes from.
    //
    // 3. EXTENSIONS=CHIP or EXTENSIONS=CELL.  This is a mosaic camera, with multiple extensions.  In this
    // case, the CONTENTS is of type METADATA, and consists of a list of contents of the file.
    //
    // In all of the above cases, the contents are specified by chip:cell:cellType triples.  It's just how the
    // contents as a *whole* are collected that changes, and what that collection means.
    //
    // We deal with each of these cases in turn.

    // Case 1: PHU=FPA and EXTENSIONS=NONE.  We need to parse the single list of chip:cell:cellType entries.
    if (strcasecmp(phuType, "FPA") == 0 && strcasecmp(extType, "NONE") == 0) {
        phdu->blankPHU = false;
        const char *contents = psMetadataLookupStr(&mdok, format, "CONTENTS"); // The contents of the file
        if (!mdok || !contents || strlen(contents) == 0) {
            psError(PS_ERR_IO, true, "Unable to find CONTENTS in the camera format configuration.\n");
            psFree(phdu);
            psFree(view);
            return NULL;
        }
        if (install && processContents(fpa, NULL, NULL, phdu, PM_FPA_LEVEL_FPA, contents, format) < 0) {
            psError(PS_ERR_IO, false, "Error setting CONTENTS");
            psFree(phdu);
            psFree(view);
            return NULL;
        }
        psFree(phdu);

        if (install && header && !pmConceptsRead(fpa, NULL, NULL, PM_CONCEPT_SOURCE_PHU, NULL)) {
            psWarning("Unable to read concepts from PHU.\n");
        }

        return view;
    }

    // In cases 2 and 3, the CONTENTS is of type METADATA, and is either a menu (if EXTENSIONS=NONE), or a
    // list of extensions otherwise.
    psMetadata *contents = psMetadataLookupMetadata(&mdok, format, "CONTENTS"); // The contents of the file
    if (!mdok || !contents) {
        if (mdok && !contents) {
            psError(PS_ERR_IO, true, "CONTENTS metadata is NULL in the camera format configuration.");
        } else {
            if(psMetadataLookup(format, "CONTENTS") != NULL) {
                psError(PS_ERR_IO, true, "CONTENTS is of wrong type in camera format configuration");
            } else {
                psError(PS_ERR_IO, true, "Unable to find CONTENTS in the camera format configuration.");
            }
        }
        psFree(phdu);
        psFree(view);
        return NULL;
    }

    // Case 2: EXTENSIONS=NONE.  We only have the PHU.  The value of CONTENT tells us a header keyword, which
    // gives us the key to the CONTENTS menu.  Or, if we've got the view specifying the PHU, we can get the
    // chip/cell directly from that.
    if (strcasecmp(extType, "NONE") == 0) {
        phdu->blankPHU = false;
        pmChip *chip = NULL;        // The chip of interest
        pmCell *cell = NULL;        // The cell of interest
        pmFPALevel level = PM_FPA_LEVEL_NONE; // Level for HDU to be added

        if (phuView) {
            // We can get the chip/cell from the view
            if (phuView->chip != -1 && phuView->chip < fpa->chips->n) {
                chip = fpa->chips->data[phuView->chip];
                level = PM_FPA_LEVEL_CHIP;
                if (phuView->cell != -1) {
                    if (phuView->cell < chip->cells->n) {
                        cell = chip->cells->data[phuView->cell];
                        level = PM_FPA_LEVEL_CELL;
                    } else {
                        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "PHU view for cell (%d) does not "
                                "correspond to camera format.\n", phuView->cell);
                        psFree(phdu);
                        return NULL;
                    }
                }
            } else {
                psError(PS_ERR_BAD_PARAMETER_VALUE, true, "PHU view for chip (%d) does not correspond to "
                        "camera format.\n", phuView->chip);
                psFree(phdu);
                return NULL;
            }
        } else {
            // Need to look up what chip we have.
            psString chipName = phuNameFromHeader("CHIP.NAME", fileInfo, header);
            psTrace("psModules.camera", 5, "This is chip %s\n", chipName);
            int chipNum = pmFPAFindChip(fpa, chipName); // Chip number
            if (chipNum == -1) {
                psError(PS_ERR_IO, true, "Unable to find chip %s in FPA.\n", chipName);
                psFree(phdu);
                psFree(view);
                return NULL;
            }
            chip = fpa->chips->data[chipNum]; // Chip of interest
            view->chip = chipNum;

            if (strcasecmp(phuType, "CHIP") == 0) {
                level = PM_FPA_LEVEL_CHIP;
            } else if (strcasecmp(phuType, "CELL") == 0) {
                level = PM_FPA_LEVEL_CELL;
                // Need to look up what cell we have.
                psString cellName = phuNameFromHeader("CELL.NAME", fileInfo, header);
                int cellNum = pmChipFindCell(chip, cellName); // Cell number
                if (cellNum == -1) {
                    psError(PS_ERR_IO, true, "Unable to find cell %s in chip %s.\n", cellName, chipName);
                    psFree(view);
                    psFree(phdu);
                    return NULL;
                }
                cell = chip->cells->data[cellNum];
                view->cell = cellNum;
                psFree(cellName);
            } else {
                // We have already dealt with the case PHU=FPA, in a special case above, so if we get here
                // it's an error.
                psError(PS_ERR_IO, true, "PHU is not FPA, CHIP or CELL.\n");
                psFree(phdu);
                psFree(view);
                return NULL;
            }
            psFree(chipName);
        }

        if (install) {
            const char *content = getContent(fileInfo, contents, chip, cell); // chip:cell:cellType triples
            if (!content || strlen(content) == 0) {
                psError(PS_ERR_IO, false, "Unable to get CONTENTS.\n");
                psFree(phdu);
                psFree(view);
                return NULL;
            }
            if (processContents(fpa, chip, cell, phdu, level, content, format) < 0) {
                psError(PS_ERR_IO, false, "Error setting CONTENTS");
                psFree(phdu);
                psFree(view);
                return NULL;
            }
        }
        psFree(phdu);

        if (install && header && !pmConceptsRead(fpa, chip, cell, PM_CONCEPT_SOURCE_PHU, NULL)) {
            psWarning("Unable to read concepts from PHU.\n");
        }
        return view;
    }

    // Case 3: EXTENSIONS=CHIP or EXTENSIONS=CELL.  We have extensions that we iterate through.  The CONTENTS
    // is a list of extensions.
    phdu->blankPHU = true;
    pmChip *chip = NULL;                // The chip of interest
    pmCell *cell = NULL;                // The cell of interest

    // First, put in the PHU
    if (strcasecmp(phuType, "FPA") == 0) {
        addHDUtoFPA(fpa, phdu);
    } else {
        // Get the chip
        psString chipName = phuNameFromHeader("CHIP.NAME", fileInfo, header); // Name of the chip
        int chipNum = pmFPAFindChip(fpa, chipName); // Chip number
        if (chipNum == -1) {
            psError(PS_ERR_IO, true, "Unable to find chip %s in FPA.\n", chipName);
            psFree(view);
            return NULL;
        }
        chip = fpa->chips->data[chipNum]; // The specified chip
        view->chip = chipNum;

        if (strcasecmp(phuType, "CHIP") == 0) {
            addHDUtoChip(chip, phdu);
        } else if (strcasecmp(phuType, "CELL") == 0) {
            psString cellName = phuNameFromHeader("CELL.NAME", fileInfo, header); // Name of the cell
            int cellNum = pmChipFindCell(chip, cellName); // Cell number
            if (cellNum == -1) {
                psError(PS_ERR_IO, true, "Unable to find cell %s in chip %s.\n", cellName, chipName);
                psFree(view);
                return NULL;
            }
            cell = chip->cells->data[cellNum]; // The specified cell
            view->cell = cellNum;
            psFree(cellName);

            addHDUtoCell(cell, phdu);
        } else {
            psError(PS_ERR_IO, true, "The format of the PHU (%s) is not FPA, CHIP or CELL.\n", phuType);
            psFree(view);
            return NULL;
        }
        psFree(chipName);
    }
    psFree(phdu);

    pmFPALevel level = hduLevel(format);// The level at which to plug in HDU

    // Now go through the contents
    psMetadataIterator *contentsIter = psMetadataIteratorAlloc(contents, PS_LIST_HEAD, NULL);
    psMetadataItem *contentsItem = NULL; // Item from contents
    while ((contentsItem = psMetadataGetAndIncrement(contentsIter))) {
        const char *extName = contentsItem->name;
        if (contentsItem->type != PS_DATA_STRING) {
            psLogMsg(__func__, PS_LOG_WARN, "CONTENTS item %s is not of type STR --- ignored.\n", extName);
            continue;
        }

        if (install) {
            pmHDU *hdu = pmHDUAlloc(extName); // The extension
            // Casting to avoid "warning: passing arg 1 of `p_psMemIncrRefCounter' discards qualifiers from
            // pointer target type"
            hdu->format = psMemIncrRefCounter((const psPtr)format);

            if (processContents(fpa, chip, cell, hdu, level, contentsItem->data.V, format) < 0) {
                psError(PS_ERR_IO, false, "Error setting CONTENTS");
                psFree(view);
                psFree(hdu);
                psFree(contentsIter);
                return NULL;
            }
            psFree(hdu);
        }
    }
    psFree(contentsIter);

    if (install) {
        if (!pmConceptsReadFPA(fpa, PM_CONCEPT_SOURCE_DEFAULTS, true, NULL)) {
            psWarning("Unable to read concepts from defaults for FPA.  Attempting to "
                      "proceed anyway.\n");
        }

        if (header && !pmConceptsRead(fpa, chip, cell, PM_CONCEPT_SOURCE_PHU, NULL)) {
            psWarning("Unable to read concepts from PHU.\n");
        }
    }

    return view;
}


bool pmFPAAddSourceFromView(pmFPA *fpa, const char *fpaname, const pmFPAview *phuView,
                            const psMetadata *format)
{
    PS_ASSERT_PTR_NON_NULL(fpa, false);
    PS_ASSERT_PTR_NON_NULL(phuView, false);
    PS_ASSERT_PTR_NON_NULL(format, false);

    pmFPAview *view = addSource(fpa, fpaname, phuView, NULL, format, true);
    bool status = (view != NULL);
    psFree(view);
    return status;
}

pmFPAview *pmFPAAddSourceFromHeader(pmFPA *fpa, psMetadata *phu, const psMetadata *format)
{
    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
    PS_ASSERT_PTR_NON_NULL(phu, NULL);
    PS_ASSERT_PTR_NON_NULL(format, NULL);

    bool mdok = true;                   // Status from metadata lookups
    psMetadata *fileInfo = psMetadataLookupMetadata(&mdok, format, "FILE"); // The file information
    if (!mdok || !fileInfo) {
        psError(PS_ERR_IO, false, "Unable to find FILE in the camera format configuration.\n");
        return NULL;
    }

    // Check the name of the FPA
    psString fpaname = phuNameFromHeader("FPA.NAME", fileInfo, phu); // New name for the FPA
    if (!fpaname || strlen(fpaname) == 0) {
        psError(PS_ERR_IO, true, "Unable to determine FPA.NAME: check for FPA.NAME in FILE in camera format");
        return NULL;
    }

    pmFPAview *view = addSource(fpa, fpaname, NULL, phu, format, true); // View of PHU, to return
    psFree(fpaname);

    return view;
}


pmFPAview *pmFPAIdentifySourceFromHeader(pmFPA *fpa, psMetadata *phu, const psMetadata *format)
{
    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
    PS_ASSERT_PTR_NON_NULL(phu, NULL);
    PS_ASSERT_PTR_NON_NULL(format, NULL);

    return addSource(fpa, NULL, NULL, phu, format, false);
}


// Print spaces to indent
#define INDENT(FILE, LEVEL) \
{ \
    for (int i = 0; i < (LEVEL); i++) { \
        fprintf(FILE, " "); \
    } \
}

void pmFPAPrint(FILE *fd, const pmFPA *fpa, bool header, bool concepts)
{
    PS_ASSERT_PTR_NON_NULL(fpa,);

    INDENT(fd, 1);
    fprintf(fd, "FPA:\n");
    if (fpa->hdu) {
        pmHDUPrint(fd, fpa->hdu, 2, header);
    }
    if (concepts) {
        psMetadataPrint(fd, fpa->concepts, 2);
    }

    psArray *chips = fpa->chips;        // Array of chips
    // Iterate over the FPA
    for (int i = 0; i < chips->n; i++) {
        INDENT(fd, 3);
        fprintf(fd, "Chip: %d\n", i);
        pmChip *chip = chips->data[i]; // The chip
        if (chip->hdu) {
            pmHDUPrint(fd, chip->hdu, 4, header);
        }
        if (concepts) {
            psMetadataPrint(fd, chip->concepts, 4);
        }

        // Iterate over the chip
        psArray *cells = chip->cells;   // Array of cells
        for (int j = 0; j < cells->n; j++) {
            INDENT(fd, 5);
            fprintf(fd, "Cell: %d\n", j);
            pmCell *cell = cells->data[j]; // The cell
            if (cell->hdu) {
                pmHDUPrint(fd, cell->hdu, 6, header);
            }
            if (concepts) {
                psMetadataPrint(fd, cell->concepts, 6);
            }

            psArray *readouts = cell->readouts; // Array of readouts
            for (int k = 0; k < readouts->n; k++) {
                pmReadout *readout = readouts->data[k]; // The readout
                INDENT(fd, 6);
                fprintf(fd, "Readout %d:\n", k);
                INDENT(fd, 7);
                fprintf(fd, "col0: %d\n", readout->col0);
                INDENT(fd, 7);
                fprintf(fd, "row0: %d\n", readout->row0);
                psImage *image = readout->image; // The image
                psList *bias = readout->bias; // The list of bias images
                if (image) {
                    INDENT(fd, 7);
                    fprintf(fd, "Image: [%d:%d,%d:%d] (%dx%d)\n", image->col0, image->col0 +
                            image->numCols, image->row0, image->row0 + image->numRows, image->numCols,
                            image->numRows);
                }
                if (bias) {
                    psListIterator *biasIter = psListIteratorAlloc(bias, PS_LIST_HEAD, false); // Iterator
                    psImage *biasImage = NULL; // Bias image from iteration
                    while ((biasImage = psListGetAndIncrement(biasIter))) {
                        INDENT(fd, 7);
                        fprintf(fd, "Bias:  [%d:%d,%d:%d] (%dx%d)\n", biasImage->col0,
                                biasImage->col0 + biasImage->numCols, biasImage->row0,
                                biasImage->row0 + biasImage->numRows, biasImage->numCols, biasImage->numRows);
                    }
                    psFree(biasIter);
                }
            } // Iterating over cell
        } // Iterating over chip
    } // Iterating over FPA

}


pmFPALevel pmFPAPHULevel(const psMetadata *format)
{
    PS_ASSERT_METADATA_NON_NULL(format, PM_FPA_LEVEL_NONE);

    bool mdok;                          // Status of MD lookup
    psMetadata *fileInfo = psMetadataLookupMetadata(&mdok, format, "FILE"); // Contents of FILE metadata
    if (!mdok || !fileInfo) {
        psError(PS_ERR_UNKNOWN, true, "Unable to find FILE in camera format configuration.\n");
        return PM_FPA_LEVEL_NONE;
    }
    const char *phu = psMetadataLookupStr(&mdok, fileInfo, "PHU"); // PHU level
    if (!mdok || !phu || strlen(phu) == 0) {
        psError(PS_ERR_UNKNOWN, true, "Unable to find PHU in FILE in camera format configuration.\n");
        return PM_FPA_LEVEL_NONE;
    }

    return pmFPALevelFromName(phu);
}

pmFPALevel pmFPAExtensionsLevel(const psMetadata *format)
{
    PS_ASSERT_METADATA_NON_NULL(format, PM_FPA_LEVEL_NONE);

    bool mdok;                          // Status of MD lookup
    psMetadata *fileInfo = psMetadataLookupMetadata(&mdok, format, "FILE"); // Contents of FILE metadata
    if (!mdok || !fileInfo) {
        psError(PS_ERR_UNKNOWN, true, "Unable to find FILE in camera format configuration.\n");
        return PM_FPA_LEVEL_NONE;
    }

    const char *extensions = psMetadataLookupStr(&mdok, fileInfo, "EXTENSIONS"); // EXTENSIONS level
    if (!mdok || !extensions || strlen(extensions) == 0) {
        psError(PS_ERR_UNKNOWN, true, "Unable to find EXTENSIONS in FILE in camera format configuration.\n");
        return PM_FPA_LEVEL_NONE;
    }

    return pmFPALevelFromName(extensions);
}

