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

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

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

#define TABLE_OF_CONTENTS "CONTENTS"    // Name for camera format metadata containing the contents
#define CHIP_TYPES "CHIPS"              // Name for camera format metadata containing the chip types
#define CELL_TYPES "CELLS"              // Name for camera format metadata containing the cell types


//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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, CELL_TYPES); // The CELLS
    if (!status || !cells) {
        psError(PS_ERR_IO, true, "Unable to find %s in camera format.\n", CELL_TYPES);
        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 int 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(string && strlen(string) > 0);
    // Must populate 'first', 'second', 'third' in order.
    assert(!second || first);
    assert(!third || second);

    int numArrays = third ? 3 : (second ? 2 : 1); // Number of arrays

    psList *values = psStringSplit(string, " ,;", true); // List of the parts

    if (first) {
        *first = psArrayAlloc(values->n);
    }
    if (second) {
        *second = psArrayAlloc(values->n);
    }
    if (third) {
        *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))) {
        psArray *fst = psStringSplitArray(value, ":", true); // First, second, third
        switch (numArrays) {
          case 3:
            psArraySet(*third, num, fst->data[2]);
          case 2:
            psArraySet(*second, num, fst->data[1]);
          case 1:
            psArraySet(*first, num, fst->data[0]);
            break;
        default:
          psAbort("Should never get here.");
        }
        num++;
        psFree(fst);
    }
    psFree(valuesIter);
    psFree(values);

    return num;
}

// 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);
    pmFPASetFileStatus(fpa, true);

    return true;
}

// 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);
    pmChipSetFileStatus(chip, true);

    return true;
}

// 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);
    pmCellSetFileStatus(cell, true);

    return true;
}


// 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 list of contents, put the HDU in the correct place and plug in the cell configuration information
static bool processContents(pmFPA *fpa,  // The FPA
                            pmChip *chip, // The chip
                            pmCell *cell, // The cell
                            pmHDU *hdu,  // The HDU to be added
                            pmFPALevel level, // The level at which to add the HDU
                            psArray *chipNames, // The chip names
                            psArray *cellNames, // The cell names
                            psArray *cellTypes, // The cell types
                            const psMetadata *format // Camera format configuration
                            )
{
    assert(fpa);
    assert(cellTypes);
    long num = cellTypes->n;            // Number of entries to add
    assert(chip || (chipNames && chipNames->n == num));
    assert(cell || (cellNames && cellNames->n == num));
    assert(format);

    if (hdu && level == PM_FPA_LEVEL_FPA) {
        if (!addHDUtoFPA(fpa, hdu)) {
            psError(PS_ERR_UNKNOWN, false, "Unable to add HDU to FPA");
            return false;
        }
    }
    // Load fpa-related concepts
    if (!pmConceptsReadFPA(fpa, PM_CONCEPT_SOURCE_DEFAULTS, false, NULL)) {
        psError(PS_ERR_UNKNOWN, false, "Unable to read concepts from camera and defaults for fpa\n");
        return false;
    }

    for (int i = 0; i < num; i++) {
        psString cellType = cellTypes->data[i]; // The type of the cell

        // Find the chip
        pmChip *newChip;                // Chip of interest
        if (chip) {
            newChip = chip;
        } else {
            psString chipName = chipNames->data[i]; // The name of the chip
            int chipNum = pmFPAFindChip(fpa, chipName); // The chip we're looking for
            if (chipNum == -1) {
                psError(PS_ERR_LOCATION_INVALID, false,
                        "Unable to find chip %s in fpa --- ignored.\n", chipName);
                return false;
            }
            newChip = fpa->chips->data[chipNum];
        }

        // 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\n");
            return false;
        }

        // Find the cell
        pmCell *newCell;                // Cell of interest
        if (cell) {
            newCell = cell;
        } else {
            psString cellName = cellNames->data[i]; // The name of the cell
            int cellNum = pmChipFindCell(newChip, cellName); // The cell we're looking for
            if (cellNum == -1) {
                psError(PS_ERR_LOCATION_INVALID, false, "Unable to find cell %s in chip --- ignored.\n",
                        cellName);
                return false;
            }
            newCell = newChip->cells->data[cellNum];
        }

        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\n");
            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 cell type %s", cellType);
            return false;
        }
    }

    return true;
}

#if 0
// 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;
}
#endif

// Find the chip of interest within the FPA, using either the view (simple) or the FITS header (bit more
// complicated).  Also updates the provided view to point to the chip if we have to find it.
static pmChip *whichChip(pmFPAview *view, // View to chip, modified
                         const pmFPA *fpa, // FPA holding chip of interest
                         const pmFPAview *phuView, // View to PHU, or NULL
                         const psMetadata *fileInfo, // FILE information from camera format
                         const psMetadata *header // FITS header, or NULL
                         )
{
    assert(view);
    assert(fpa);
    assert(phuView || header);
    assert(fileInfo);

    if (phuView) {
        return pmFPAviewThisChip(phuView, fpa);
    }

    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_UNKNOWN, true, "Unable to find chip %s in FPA.\n", chipName);
        return NULL;
    }
    psFree(chipName);
    view->chip = chipNum;
    return fpa->chips->data[chipNum];
}

// Find the cell of interest within the chip, using either the view (simple) or the FITS header (bit more
// complicated).  Also updates the provided view to point to the cell if we have to find it.
static pmCell *whichCell(pmFPAview *view, // View to cell
                         const pmChip *chip, // Chip holding cell of interest
                         const pmFPAview *phuView, // View to PHU, or NULL
                         const psMetadata *fileInfo, // FILE information from camera format
                         const psMetadata *header // FITS header, or NULL
                         )
{
    assert(view);
    assert(chip);
    assert(phuView || header);
    assert(fileInfo);

    pmFPA *fpa = chip->parent;          // The parent FPA

    if (phuView) {
        return pmFPAviewThisCell(phuView, fpa);
    }

    psString cellName = phuNameFromHeader("CELL.NAME", fileInfo, header);
    psTrace("psModules.camera", 5, "This is cell %s\n", cellName);
    int cellNum = pmChipFindCell(chip, cellName); // Cell number
    if (cellNum == -1) {
        psError(PS_ERR_UNKNOWN, true, "Unable to find cell %s in chip.\n", cellName);
        return NULL;
    }
    view->cell = cellNum;
    return chip->cells->data[cellNum];
}


// PHU=FPA and EXTENSIONS=CHIP:
// TABLE_OF_CONTENTS(METADATA) has a list of extensions, each with a chipName:chipType.
// CHIP_TYPES(METADATA) has a list of chip types, each with cellName:cellType
static bool addSource_FPA_CHIP(pmFPA *fpa, // FPA to which to add
                               const psMetadata *format // The camera format
                               )
{
    assert(fpa);
    assert(format);

    psMetadata *contents = psMetadataLookupMetadata(NULL, format, TABLE_OF_CONTENTS); // The contents
    if (!contents) {
        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find %s in camera format.", TABLE_OF_CONTENTS);
        return false;
    }

    psMetadata *chips = psMetadataLookupMetadata(NULL, format, CHIP_TYPES); // The chip types
    if (!chips) {
        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find %s in camera format.", CHIP_TYPES);
        return false;
    }

    // Iterate over all extensions
    psMetadataIterator *contentsIter = psMetadataIteratorAlloc(contents, PS_LIST_HEAD, NULL); // Iterator
    psMetadataItem *item;               // Item from iteration
    while ((item = psMetadataGetAndIncrement(contentsIter))) {
        if (item->type != PS_DATA_STRING) {
            psError(PS_ERR_BAD_PARAMETER_TYPE, true,
                    "Type for %s (%x) in %s METADATA in camera format is not STR",
                    item->name, item->type, TABLE_OF_CONTENTS);
            psFree(contentsIter);
            return false;
        }

        const char *extname = item->name; // Extension name
        pmHDU *hdu = pmHDUAlloc(extname); // HDU for this extension
        // Casting to avoid "warning: passing arg 1 of `p_psMemIncrRefCounter' discards qualifiers from
        // pointer target type"
        hdu->format = psMemIncrRefCounter((const psPtr)format);

        // What's in the extension?  It's specified by chipName:chipType
        // Assume that an extension contains only a single chip, instead of multiple chips

        psString extContents = item->data.str; // Contents of extension
        psArray *chipNames = NULL;
        psArray *chipTypes = NULL;
        if (parseContent(&chipNames, &chipTypes, NULL, extContents) != 1) {
            psError(PS_ERR_UNKNOWN, false,
                    "Unable to parse chipName:chipType in %s of %s in camera format",
                    extname, TABLE_OF_CONTENTS);
        }

        psString chipName = psMemIncrRefCounter(chipNames->data[0]); // Name of chip
        psString chipType = psMemIncrRefCounter(chipTypes->data[0]); // Type of chip
        psFree(chipNames);
        psFree(chipTypes);

        // Get the chip
        int chipNum = pmFPAFindChip(fpa, chipName); // Chip number
        if (chipNum == -1) {
            psError(PS_ERR_UNKNOWN, false, "Unable to find chip %s in FPA.\n", chipName);
            psFree(hdu);
            psFree(contentsIter);
            return false;
        }
        pmChip *chip = fpa->chips->data[chipNum]; // Chip of interest

        // What's in the chip?

        psString chipContents = psMetadataLookupStr(NULL, chips, chipType); // Contents of the chip
        if (!chipContents) {
            psError(PS_ERR_UNEXPECTED_NULL, false,
                    "Unable to find chip type %s (for extension %s) in %s of camera format",
                    chipType, extname, CHIP_TYPES);
            psFree(chipName);
            psFree(chipType);
            psFree(hdu);
            psFree(contentsIter);
            return false;
        }

        psArray *cellNames = NULL;      // Cell names
        psArray *cellTypes = NULL;      // Cell types
        if (parseContent(&cellNames, &cellTypes, NULL, chipContents) == 0) {
            psError(PS_ERR_BAD_PARAMETER_VALUE, false,
                    "Unable to parse chip contents (within %s->%s in camera format; for chip %s) "
                    "as cellName:cellType", CHIP_TYPES, chipType, chipName);
            psFree(cellNames);
            psFree(cellTypes);
            psFree(chipName);
            psFree(chipType);
            psFree(hdu);
            psFree(contentsIter);
            return false;
        }

        if (!processContents(fpa, chip, NULL, hdu, PM_FPA_LEVEL_CHIP, NULL, cellNames, cellTypes, format)) {
            psError(PS_ERR_UNKNOWN, false, "Unable to set contents for chip %s from camera format.",
                    chipName);
            psFree(cellNames);
            psFree(cellTypes);
            psFree(chipName);
            psFree(chipType);
            psFree(hdu);
            psFree(contentsIter);
            return false;
        }

        psFree(cellNames);
        psFree(cellTypes);
        psFree(chipName);
        psFree(chipType);

        psFree(hdu);                    // Drop reference
    }
    psFree(contentsIter);

    return true;
}

// PHU=FPA and EXTENSIONS=CELL:
// TABLE_OF_CONTENTS(METADATA) has a list of extensions, each with a chipName:cellName:cellType.
static bool addSource_FPA_CELL(pmFPA *fpa, // FPA to which to add
                               const psMetadata *format // The camera format
                               )
{
    assert(fpa);
    assert(format);

    psMetadata *contents = psMetadataLookupMetadata(NULL, format, TABLE_OF_CONTENTS); // The contents
    if (!contents) {
        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find %s in camera format.", TABLE_OF_CONTENTS);
        return false;
    }

    // Iterate over all extensions
    psMetadataIterator *contentsIter = psMetadataIteratorAlloc(contents, PS_LIST_HEAD, NULL); // Iterator
    psMetadataItem *item;               // Item from iteration
    while ((item = psMetadataGetAndIncrement(contentsIter))) {
        if (item->type != PS_DATA_STRING) {
            psError(PS_ERR_BAD_PARAMETER_TYPE, true,
                    "Type for %s (%x) in %s METADATA in camera format is not STR",
                    item->name, item->type, TABLE_OF_CONTENTS);
            psFree(contentsIter);
            return false;
        }

        const char *extname = item->name; // Extension name
        pmHDU *hdu = pmHDUAlloc(extname); // HDU for this extension
        // Casting to avoid "warning: passing arg 1 of `p_psMemIncrRefCounter' discards qualifiers from
        // pointer target type"
        hdu->format = psMemIncrRefCounter((const psPtr)format);

        // What's in the extension?  It's specified by (possibly multiple) chipName:cellName:cellType

        psArray *chipNames = NULL;      // Chip names
        psArray *cellNames = NULL;      // Cell names
        psArray *cellTypes = NULL;      // Cell types
        if (parseContent(&chipNames, &cellNames, &cellTypes, item->data.str) == 0) {
            psError(PS_ERR_BAD_PARAMETER_VALUE, false,
                    "Unable to parse extension contents (within %s->%s in camera format) as "
                    "chipName:cellName:cellType", TABLE_OF_CONTENTS, extname);
            psFree(chipNames);
            psFree(cellNames);
            psFree(cellTypes);
            psFree(hdu);
            psFree(contentsIter);
            return false;
        }

        if (!processContents(fpa, NULL, NULL, hdu, PM_FPA_LEVEL_CELL, chipNames, cellNames, cellTypes,
                             format)) {
            psError(PS_ERR_UNKNOWN, false, "Unable to set contents from camera format.");
            psFree(chipNames);
            psFree(cellNames);
            psFree(cellTypes);
            psFree(hdu);
            psFree(contentsIter);
            return false;
        }

        psFree(chipNames);
        psFree(cellNames);
        psFree(cellTypes);

        psFree(hdu);                    // Drop reference
    }
    psFree(contentsIter);

    return true;
}

// PHU=FPA and EXTENSIONS=NONE:
// TABLE_OF_CONTENTS(STR) has a list of chipName:cellName:cellType.
static bool addSource_FPA_NONE(pmFPA *fpa, // FPA to which to add
                               const psMetadata *format // The camera format
                               )
{
    assert(fpa);
    assert(format);

    psString contents = psMetadataLookupStr(NULL, format, TABLE_OF_CONTENTS); // The contents
    if (!contents) {
        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find %s in camera format.", TABLE_OF_CONTENTS);
        return false;
    }

    // What's in the file?  It's specified by (possibly multiple) chipName:cellName:cellType

    psArray *chipNames = NULL;          // Chip names
    psArray *cellNames = NULL;          // Cell names
    psArray *cellTypes = NULL;          // Cell types
    if (parseContent(&chipNames, &cellNames, &cellTypes, contents) == 0) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, false,
                "Unable to parse contents (within %s in camera format) as chipName:cellName:cellType",
                TABLE_OF_CONTENTS);
        psFree(chipNames);
        psFree(cellNames);
        psFree(cellTypes);
        return false;
    }

    if (!processContents(fpa, NULL, NULL, NULL, PM_FPA_LEVEL_NONE, chipNames, cellNames, cellTypes,
                         format)) {
        psError(PS_ERR_UNKNOWN, false, "Unable to set contents from camera format.");
        psFree(chipNames);
        psFree(cellNames);
        psFree(cellTypes);
        return false;
    }

    psFree(chipNames);
    psFree(cellNames);
    psFree(cellTypes);

    return true;
}


// PHU=CHIP and EXTENSIONS=CELL:
// TABLE_OF_CONTENTS(METADATA) has a menu of contents, each with a chipType.
// CHIP_TYPES(METADATA) has a list of chip types, each with extension(METADATA) with cellName:cellType
static bool addSource_CHIP_CELL(pmChip *chip, // Chip to which to add
                                const psMetadata *format // The camera format
                                )
{
    assert(chip);
    assert(format);

    psMetadata *contents = psMetadataLookupMetadata(NULL, format, TABLE_OF_CONTENTS); // The contents
    if (!contents) {
        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find %s in camera format.", TABLE_OF_CONTENTS);
        return false;
    }

    psMetadata *chips = psMetadataLookupMetadata(NULL, format, CHIP_TYPES); // The chip types
    if (!chips) {
        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find %s in camera format.", CHIP_TYPES);
        return false;
    }

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

    const char *chipType = getContent(fileInfo, contents, chip, NULL); // The chip type

    // What's in the chip?

    psMetadata *chipContents = psMetadataLookupMetadata(NULL, chips, chipType); // Contents of the chip
    if (!chipContents) {
        psError(PS_ERR_UNEXPECTED_NULL, false,
                "Unable to find chip type %s in %s of camera format",
                chipType, CHIP_TYPES);
        return false;
    }

    pmFPA *fpa = chip->parent;          // The parent FPA

    psMetadataIterator *contentsIter = psMetadataIteratorAlloc(chipContents, PS_LIST_HEAD, NULL); // Iterator
    psMetadataItem *contentItem;        // Content, from iteration
    while ((contentItem = psMetadataGetAndIncrement(contentsIter))) {
        if (contentItem->type != PS_DATA_STRING) {
            psError(PS_ERR_BAD_PARAMETER_TYPE, true,
                    "Type for %s (%x) in %s METADATA in camera format is not STR",
                    contentItem->name, contentItem->type, TABLE_OF_CONTENTS);
            psFree(contentsIter);
            return false;
        }

        const char *extname = contentItem->name; // Extension name
        pmHDU *hdu = pmHDUAlloc(extname); // HDU for this extension
        // Casting to avoid "warning: passing arg 1 of `p_psMemIncrRefCounter' discards qualifiers from
        // pointer target type"
        hdu->format = psMemIncrRefCounter((const psPtr)format);

        psArray *cellNames = NULL;      // Cell names
        psArray *cellTypes = NULL;      // Cell types
        if (parseContent(&cellNames, &cellTypes, NULL, contentItem->data.str) == 0) {
            psError(PS_ERR_BAD_PARAMETER_VALUE, false,
                    "Unable to parse chip contents (within %s->%s in camera format) "
                    "as cellName:cellType", CHIP_TYPES, chipType);
            psFree(cellNames);
            psFree(cellTypes);
            psFree(hdu);
            psFree(contentsIter);
            return false;
        }

        if (!processContents(fpa, chip, NULL, hdu, PM_FPA_LEVEL_CELL, NULL, cellNames, cellTypes, format)) {
            psError(PS_ERR_UNKNOWN, false, "Unable to set contents for chip type %s from camera format.",
                    chipType);
            psFree(cellNames);
            psFree(cellTypes);
            psFree(hdu);
            psFree(contentsIter);
            return false;
        }

        psFree(cellNames);
        psFree(cellTypes);

        psFree(hdu);                    // Drop reference
    }
    psFree(contentsIter);

    return true;
}

// PHU=CHIP and EXTENSIONS=NONE:
// TABLE_OF_CONTENTS(METADATA) has a menu of contents, each with a chipName:chipType.
// CHIP_TYPES(METADATA) has a list of chip types, each with cellName:cellType
static bool addSource_CHIP_NONE(pmChip *chip, // Chip to which to add
                                const psMetadata *format // The camera format
                                )
{
    assert(chip);
    assert(format);

    psMetadata *contents = psMetadataLookupMetadata(NULL, format, TABLE_OF_CONTENTS); // The contents
    if (!contents) {
        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find %s in camera format.", TABLE_OF_CONTENTS);
        return false;
    }

    psMetadata *chips = psMetadataLookupMetadata(NULL, format, CHIP_TYPES); // The chip types
    if (!chips) {
        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find %s in camera format.", CHIP_TYPES);
        return false;
    }

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

    pmFPA *fpa = chip->parent;          // Parent FPA

    const char *chipType = getContent(fileInfo, contents, chip, NULL); // The chip type

    // What's in the chip?

    psString chipContents = psMetadataLookupStr(NULL, chips, chipType); // Contents of the chip
    if (!chipContents) {
        psError(PS_ERR_UNEXPECTED_NULL, false,
                "Unable to find chip type %s in %s of camera format", chipType, CHIP_TYPES);
        return false;
    }

    psArray *cellNames = NULL;      // Cell names
    psArray *cellTypes = NULL;      // Cell types
    if (parseContent(&cellNames, &cellTypes, NULL, chipContents) == 0) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, false,
                "Unable to parse chip contents (within %s->%s in camera format) "
                "as cellName:cellType", CHIP_TYPES, chipType);
        psFree(cellNames);
        psFree(cellTypes);
        return false;
    }

    if (!processContents(fpa, chip, NULL, NULL, PM_FPA_LEVEL_NONE, NULL, cellNames, cellTypes, format)) {
        psError(PS_ERR_UNKNOWN, false, "Unable to set contents for chip type %s from camera format.",
                chipType);
        psFree(cellNames);
        psFree(cellTypes);
        return false;
    }

    psFree(cellNames);
    psFree(cellTypes);

    return true;
}

// PHU=CELL and EXTENSIONS=NONE:
// TABLE_OF_CONTENTS(METADATA) has a menu of contents, each with a cellName:cellType
static bool addSource_CELL_NONE(pmCell *cell, // Cell to which to add
                                const psMetadata *format // The camera format
                                )
{
    assert(cell);
    assert(format);

    psMetadata *contents = psMetadataLookupMetadata(NULL, format, TABLE_OF_CONTENTS); // The contents
    if (!contents) {
        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find %s in camera format.", TABLE_OF_CONTENTS);
        return false;
    }

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

    pmChip *chip = cell->parent;        // Parent chip
    pmFPA *fpa = chip->parent;          // Parent FPA

    const char *content = getContent(fileInfo, contents, chip, NULL); // Content of cell

    psArray *cellNames = NULL;      // Cell names
    psArray *cellTypes = NULL;      // Cell types
    if (parseContent(&cellNames, &cellTypes, NULL, content) != 1) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, false,
                "Unable to parse cell contents (%s) as cellName:cellType", content);
        psFree(cellNames);
        psFree(cellTypes);
        return false;
    }

    if (!processContents(fpa, chip, cell, NULL, PM_FPA_LEVEL_NONE, NULL, cellNames, cellTypes, format)) {
        psError(PS_ERR_UNKNOWN, false, "Unable to set contents for cell from camera format.");
        psFree(cellNames);
        psFree(cellTypes);
        return false;
    }

    psFree(cellNames);
    psFree(cellTypes);

    return true;
}


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

    // Case    PHU     EXTENSIONS     Description
    // ====    ===     ==========     ===========
    // 1.      FPA     CHIP           CONTENTS(METADATA) has a list of extensions, each with a chip type.
    //                                CHIPS(METADATA) has a list of chip types, each with cell:type
    // 2.      FPA     CELL           CONTENTS(METADATA) has a list of extensions, each with chip:cell:type
    //                                No need for CHIPS.
    // 3.      FPA     NONE           CONTENTS(STRING) has a list of extensions, chip:cell:type
    //                                No need for CHIPS
    // 4.      CHIP    CELL           CONTENTS(METADATA) is a menu, each with a chip type
    //                                CHIPS(METADATA) has a list of chip types(METADATA), containg a list of
    //                                extensions.
    // 5.      CHIP    NONE           CONTENTS(METADATA) is a menu, each with a chip type
    //                                CHIPS(METADATA) has a list of chip types(STRING) with cell:type
    // 6.      CELL    NONE           CONTENTS(METADATA) is a menu, each with a cell type.
    //                                No need for CHIPS.


    pmFPALevel phuLevel = pmFPALevelFromName(phuType); // Level for PHU
    pmFPALevel extLevel = pmFPALevelFromName(extType); // Level for extensions

    switch (phuLevel) {
      case PM_FPA_LEVEL_FPA: {
          // We don't have to work out where the PHU is --- there's only one FPA.
          // 'view' already points to the FPA.
          switch (extLevel) {
            case PM_FPA_LEVEL_CHIP:
              phdu->blankPHU = true;
              if (install) {
                  if (!addHDUtoFPA(fpa, phdu) || !addSource_FPA_CHIP(fpa, format) ||
                      !pmConceptsReadFPA(fpa, PM_CONCEPT_SOURCE_DEFAULTS | PM_CONCEPT_SOURCE_PHU,
                                         true, NULL)) {
                      psError(PS_ERR_UNKNOWN, false, "Unable to add source.");
                      psFree(phdu);
                      psFree(view);
                      return NULL;
                  }
              }
              psFree(phdu);
              return view;
            case PM_FPA_LEVEL_CELL:
              if (install) {
                  phdu->blankPHU = true;
                  if (!addHDUtoFPA(fpa, phdu) || !addSource_FPA_CELL(fpa, format) ||
                      !pmConceptsReadFPA(fpa, PM_CONCEPT_SOURCE_DEFAULTS | PM_CONCEPT_SOURCE_PHU,
                                         true, NULL)) {
                      psError(PS_ERR_UNKNOWN, false, "Unable to add source.");
                      psFree(phdu);
                      psFree(view);
                      return NULL;
                  }
              }
              psFree(phdu);
              return view;
            case PM_FPA_LEVEL_NONE:
              if (install) {
                  phdu->blankPHU = false;
                  if (!addHDUtoFPA(fpa, phdu) || !addSource_FPA_NONE(fpa, format) ||
                      !pmConceptsReadFPA(fpa, PM_CONCEPT_SOURCE_DEFAULTS | PM_CONCEPT_SOURCE_PHU,
                                         true, NULL)) {
                      psError(PS_ERR_UNKNOWN, false, "Unable to add source.");
                      psFree(phdu);
                      psFree(view);
                      return NULL;
                  }
              }
              psFree(phdu);
             return view;
            default:
              psError(PS_ERR_BAD_PARAMETER_VALUE, true,
                      "EXTENSIONS level (%s) incompatible with PHU level (FPA)", extType);
              psFree(phdu);
              psFree(view);
              return NULL;
          }
          break;
      }
      case PM_FPA_LEVEL_CHIP: {
          // Which chip is our PHU?
          pmChip *chip = whichChip(view, fpa, phuView, fileInfo, header); // Chip of interest
          if (!chip) {
              psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find chip to add.");
              psFree(phdu);
              psFree(view);
              return NULL;
          }
          switch (extLevel) {
            case PM_FPA_LEVEL_CELL:
              if (install) {
                  phdu->blankPHU = true;
                  if (!addHDUtoChip(chip, phdu) || !addSource_CHIP_CELL(chip, format) ||
                      !pmConceptsReadChip(chip, PM_CONCEPT_SOURCE_DEFAULTS | PM_CONCEPT_SOURCE_PHU,
                                          true, true, NULL)) {
                      psError(PS_ERR_UNKNOWN, false, "Unable to add source.");
                      psFree(phdu);
                      psFree(view);
                      return NULL;
                  }
              }
              psFree(phdu);
              return view;
            case PM_FPA_LEVEL_NONE:
              if (install) {
                  phdu->blankPHU = false;
                  if (!addHDUtoChip(chip, phdu) || !addSource_CHIP_NONE(chip, format) ||
                      !pmConceptsReadChip(chip, PM_CONCEPT_SOURCE_DEFAULTS | PM_CONCEPT_SOURCE_PHU,
                                          true, true, NULL)) {
                      psError(PS_ERR_UNKNOWN, false, "Unable to add source.");
                      psFree(phdu);
                      psFree(view);
                      return NULL;
                  }
              }
              psFree(phdu);
              return view;
            default:
              psError(PS_ERR_BAD_PARAMETER_VALUE, true,
                      "EXTENSIONS level (%s) incompatible with PHU level (CHIP)", extType);
              return NULL;
          }
          break;
      }
      case PM_FPA_LEVEL_CELL: {
          pmChip *chip = whichChip(view, fpa, phuView, fileInfo, header); // Chip of interest
          if (!chip) {
              psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find chip to add.");
              psFree(phdu);
              psFree(view);
              return NULL;
          }
          pmCell *cell = whichCell(view, chip, phuView, fileInfo, header); // Cell of interest
          if (!cell) {
              psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find cell to add.");
              psFree(phdu);
              psFree(view);
              return NULL;
          }
          if (extLevel != PM_FPA_LEVEL_NONE) {
              psError(PS_ERR_BAD_PARAMETER_VALUE, true,
                      "EXTENSIONS level (%s) incompatible with PHU level (CELL)", extType);
              return NULL;
          }
          if (install) {
              phdu->blankPHU = false;
              if (!addHDUtoCell(cell, phdu) || !addSource_CELL_NONE(cell, format) ||
                  !pmConceptsReadCell(cell, PM_CONCEPT_SOURCE_DEFAULTS | PM_CONCEPT_SOURCE_PHU,
                                      true, NULL)) {
                  psError(PS_ERR_UNKNOWN, false, "Unable to add source.");
                  psFree(phdu);
                  psFree(view);
                  return NULL;
              }
          }
          psFree(phdu);
          return view;
          break;
      }
      default:
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Bad PHU level: %s", phuType);
        return NULL;
    }

    return NULL;
}

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

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
                psImage *mask = readout->mask; // The mask
                psImage *weight = readout->weight; // The weight
                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);
                }
                if (mask) {
                    INDENT(fd, 7);
                    fprintf(fd, "Mask: [%d:%d,%d:%d] (%dx%d)\n", mask->col0, mask->col0 +
                            mask->numCols, mask->row0, mask->row0 + mask->numRows, mask->numCols,
                            mask->numRows);
                }
                if (weight) {
                    INDENT(fd, 7);
                    fprintf(fd, "Weight: [%d:%d,%d:%d] (%dx%d)\n", weight->col0, weight->col0 +
                            weight->numCols, weight->row0, weight->row0 + weight->numRows, weight->numCols,
                            weight->numRows);
                }
            } // 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);
}

