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

#include <stdio.h>
#include <string.h>
#include <strings.h>		/* for strn?casecmp */
#include <pslib.h>

#include "pmConfig.h"
#include "pmDetrendDB.h"

#include "pmHDU.h"
#include "pmFPA.h"
#include "pmFPALevel.h"
#include "pmFPAMaskWeight.h"
#include "pmFPAview.h"
#include "pmFPAFlags.h"
#include "pmFPAfile.h"
#include "pmFPACopy.h"
#include "pmFPARead.h"
#include "pmFPAWrite.h"
#include "pmFPAfileIO.h"
#include "pmFPAfileFitsIO.h"
#include "pmPeaks.h"
#include "pmMoments.h"
#include "pmResiduals.h"
#include "pmModel.h"
#include "pmSource.h"
#include "pmSourceIO.h"
#include "pmGrowthCurve.h"
#include "pmResiduals.h"
#include "pmPSF.h"
#include "pmPSF_IO.h"
#include "pmFPA_JPEG.h"
#include "pmSourcePlots.h"
#include "pmFPAConstruct.h"

// attempt create, read, write, close, or free pmFPAfiles available in files files are
// automatically opened before they are read.  In the case of MEF files, the PHU is
// read when the file is opened and written before the first extension is written.
bool pmFPAfileIOChecks (pmConfig *config, const pmFPAview *view, pmFPAfilePlace place)
{
    PS_ASSERT_PTR_NON_NULL(config, false);
    PS_ASSERT_PTR_NON_NULL(config->files, false);
    PS_ASSERT_PTR_NON_NULL(view, false);

    psMetadata *files = config->files;

    // attempt to perform all create, read, write, close operations
    psMetadataItem *item = NULL;
    psMetadataIterator *iter = psMetadataIteratorAlloc (files, PS_LIST_HEAD, NULL);
    while ((item = psMetadataGetAndIncrement (iter)) != NULL) {
        pmFPAfile *file = item->data.V;

        switch (place) {
        case PM_FPA_BEFORE:
            if (!pmFPAfileRead (file, view, config)) {
                psError(PS_ERR_IO, false, "failed READ in FPA_BEFORE block for %s", file->name);
                goto failure;
            }
            if (!pmFPAfileCreate(file, view, config)) {
                psError(PS_ERR_IO, false, "failed CREATE in FPA_BEFORE block for %s", file->name);
                goto failure;
            }
            break;
        case PM_FPA_AFTER:
            if (!pmFPAfileWrite (file, view, config)) {
                psError(PS_ERR_IO, false, "failed WRITE in FPA_AFTER block for %s", file->name);
                goto failure;
            }
            if (!pmFPAfileClose(file, view)) {
                psError(PS_ERR_IO, false, "failed CLOSE in FPA_AFTER block for %s", file->name);
                goto failure;
            }
            break;
        default:
            psAbort("You can't get here");
        }
    }

    // attempt to free data that is no longer needed
    psMetadataIteratorSet (iter, PS_LIST_HEAD);
    while ((item = psMetadataGetAndIncrement (iter)) != NULL) {
        pmFPAfile *file = item->data.V;

        switch (place) {
        case PM_FPA_BEFORE:
            break;
        case PM_FPA_AFTER:
            if (!pmFPAfileFreeData(file, view)) {
                if (!psMetadataRemoveKey(files, file->name)) {
                    psError(PS_ERR_IO, false, "failed to remove %s in FPA_AFTER block", file->name);
                    goto failure;
                }
            }
            break;
        default:
            psAbort("You can't get here");
        }
    }
    psFree (iter);
    return true;

failure:
    psFree (iter);
    return false;
}

// read the file, if necessary and possible
bool pmFPAfileRead(pmFPAfile *file, const pmFPAview *view, pmConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(file, false);
    PS_ASSERT_PTR_NON_NULL(view, false);

    // an internal file should not be sent here (should not be left on config->files)
    PS_ASSERT(file->mode != PM_FPA_MODE_INTERNAL, false);

    // skip the following states
    if (file->state & PM_FPA_STATE_INACTIVE) {
        psTrace("psModules.camera", 6, "skip read for %s, file is inactive", file->name);
        return true;
    }
    if (file->mode != PM_FPA_MODE_READ) {
        psTrace("psModules.camera", 6, "skip read for %s, mode is not READ", file->name);
        return true;
    }

    // get the current level
    pmFPALevel level = pmFPAviewLevel (view);

    // do we need to read this file? defer until we read the correct level
    if (level != file->dataLevel) {
        psTrace("psModules.camera", 6, "skip reading of %s at this level %s: dataLevel is %s",
                file->name, pmFPALevelToName(level), pmFPALevelToName(file->dataLevel));
        return true;
    }

    // do we need to open this file?
    if (level >= file->fileLevel) {
        // we are allowed to open a file at a level which is not the fileLevel, but we need to
        // supply a view at the fileLevel for the file lookup functions below
        pmFPAview *fileView = pmFPAviewForLevel (file->fileLevel, view);
        if (!pmFPAfileOpen (file, fileView, config)) {
            psError(PS_ERR_IO, false, "failed to open file %s (%s)", file->filename, file->name);
            psFree (fileView);
            return false;
        }
        psFree (fileView);
    }

    // We need to read it --- double-check it's open!
    if (file->state == PM_FPA_STATE_CLOSED) {
        psError(PS_ERR_IO, false, "failed to open file %s when attempting to read", file->name);
        return false;
    }

    // select a reading method
    bool status = true;
    switch (file->type) {
      case PM_FPA_FILE_IMAGE:
        status = pmFPAviewReadFitsImage(view, file);
        break;
      case PM_FPA_FILE_MASK:
        status = pmFPAviewReadFitsMask(view, file);
        break;
      case PM_FPA_FILE_WEIGHT:
        status = pmFPAviewReadFitsWeight(view, file);
        break;
      case PM_FPA_FILE_HEADER:
        status = pmFPAviewReadFitsHeaderSet(view, file);
        break;
      case PM_FPA_FILE_FRINGE:
        status = pmFPAviewReadFitsImage (view, file);
        if (status) {
            if (!pmFPAviewReadFitsTable(view, file, "FRINGE")) {
                psError(PS_ERR_UNKNOWN, false, "Unable to read fringe data from %s.\n", file->filename);
                return false;
            }
        }
        break;
      case PM_FPA_FILE_SX:
      case PM_FPA_FILE_RAW:
      case PM_FPA_FILE_OBJ:
      case PM_FPA_FILE_CMP:
      case PM_FPA_FILE_CMF:
        status = pmFPAviewReadObjects (view, file, config);
        break;
      case PM_FPA_FILE_PSF:
        status = pmFPAviewReadPSFmodel (view, file, config);
        break;
      case PM_FPA_FILE_JPEG:
      case PM_FPA_FILE_KAPA:
        break;
      default:
        psError(PS_ERR_IO, true, "warning: type mismatch; saw type %d (%s)", file->type, file->name);
        return false;
    }
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to read %s (%s)\n", file->filename, file->name);
        return false;
    }
    psTrace ("psModules.camera", 5, "read %s (%s) (%d:%d:%d)\n", file->filename, file->name, view->chip, view->cell, view->readout);
    return true;
}

// create the data elements (headers, images) appropriate for this view
bool pmFPAfileCreate (pmFPAfile *file, const pmFPAview *view, const pmConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(file, false);
    PS_ASSERT_PTR_NON_NULL(view, false);

    // these are not error conditions; these are state tests
    if (file->state & PM_FPA_STATE_INACTIVE) {
        psTrace("psModules.camera", 6, "skip create for inactive file %s", file->name);
        return true;
    }
    if (file->mode != PM_FPA_MODE_WRITE) {
        psTrace("psModules.camera", 6, "skip create for non-write file %s", file->name);
        return true;
    }

    // an internal file should not be returned to here
    PS_ASSERT(file->mode != PM_FPA_MODE_INTERNAL, false);

    // get the current level
    pmFPALevel level = pmFPAviewLevel (view);

    // don't create the file if the src (FPA) is not defined
    if (file->src == NULL) {
        psTrace("psModules.camera", 6, "skip create for FPA without src FPA for %s", file->name);
        return true;
    }

    // do we need to write this file?
    if (level != file->fileLevel || file->mosaicLevel != PM_FPA_LEVEL_NONE) {
        psTrace("psModules.camera", 6, "skip creation of %s at this level %s: fileLevel is %s",
                file->name, pmFPALevelToName(level), pmFPALevelToName(file->fileLevel));
        return true;
    }

    switch (file->type) {
    case PM_FPA_FILE_IMAGE:
    case PM_FPA_FILE_MASK:
    case PM_FPA_FILE_WEIGHT:
    case PM_FPA_FILE_FRINGE: {
            // create FPA structure component based on view
            psMetadata *format = file->format; // Camera format configuration
            if (!format) {
                format = config->format;
            }

            pmFPA *nameSource = file->src; // Source of FPA.NAME
            if (!nameSource) {
                nameSource = file->fpa;
            }
            bool mdok;                  // Status of MD lookup
            const char *fpaname = psMetadataLookupStr(&mdok, nameSource->concepts, "FPA.NAME"); // Name of FPA

            pmFPAAddSourceFromView(file->fpa, fpaname, view, format);
            psTrace ("psModules.camera", 5, "created fpa data elements for %s (%s) (%d:%d:%d)\n", file->name, file->name, view->chip, view->cell, view->readout);
            break;
    }
    case PM_FPA_FILE_HEADER:
      psAbort ("Create not defined for HEADER");
      break;
    case PM_FPA_FILE_SX:
    case PM_FPA_FILE_RAW:
    case PM_FPA_FILE_OBJ:
    case PM_FPA_FILE_CMP:
    case PM_FPA_FILE_CMF:
    case PM_FPA_FILE_PSF:
    case PM_FPA_FILE_JPEG:
    case PM_FPA_FILE_KAPA:
        break;

    default:
        psError(PS_ERR_IO, true, "Unsupported type for %s: %d", file->name, file->type);
        return false;
    }
    return true;
}

bool pmFPAfileWrite(pmFPAfile *file, const pmFPAview *view, pmConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(file, false);
    PS_ASSERT_PTR_NON_NULL(view, false);

    if (file->state & PM_FPA_STATE_INACTIVE) {
        psTrace("psModules.camera", 6, "skip write for %s, files is inactive", file->name);
        return true;
    }

    if (file->mode != PM_FPA_MODE_WRITE) {
        psTrace("psModules.camera", 6, "skip write for %s, mode is not WRITE", file->name);
        return true;
    }

    // an internal file should not be returned to here
    if (file->mode == PM_FPA_MODE_INTERNAL) {
        psError(PS_ERR_IO, true, "File is mode PM_FPA_MODE_INTERNAL");
        return false;
    }

    if (!file->save) {
        psTrace("psModules.camera", 6, "skip write for %s, save is FALSE", file->name);
        return true;
    }

    // get the current level
    pmFPALevel level = pmFPAviewLevel (view);

    // the effective dataLevel (for mosaics, we have preserved the original dataLevel)
    pmFPALevel dataLevel = (file->mosaicLevel != PM_FPA_LEVEL_NONE) ? file->mosaicLevel : file->dataLevel;

    // do we need to write this file?
    if (level != dataLevel) {
        psTrace("psModules.camera", 6, "skip writing of %s at this level %s: dataLevel is %s",
                file->name, pmFPALevelToName(level), pmFPALevelToName(dataLevel));
        return true;
    }

    // do we have data to write at this level?
    if (!pmFPAviewCheckDataStatus (file->fpa, view)) {
        psTrace("psModules.camera", 6, "skip write for %s, no data for this entry", file->name);
        return true;
    }

    // note that for CMF and PSF, the test above is not sufficient to determine if there
    // is actually any data to write out.  this is because these types of output files
    // have their data stored on the readout->analysis metadata structure of another
    // (existing) fpa
    if (file->type == PM_FPA_FILE_PSF) {
      if (!pmFPAviewCheckDataStatusForPSFmodel (view, file)) {
        psTrace("psModules.camera", 6, "skip write for %s, no data for this entry", file->name);
        return true;
      }
    }
    if (file->type == PM_FPA_FILE_CMF) {
      if (!pmFPAviewCheckDataStatusForSources (view, file)) {
        psTrace("psModules.camera", 6, "skip write for %s, no data for this entry", file->name);
        return true;
      }
    }

    // open the file if not yet opened
    // XXX do we need to test mosaicLevel?
    if (level >= file->fileLevel) {
        // we are allowed to open a file at a level which is not the fileLevel, but
        // we need to supply view at the fileLevel for the file lookup functions below
        pmFPAview *fileView = pmFPAviewForLevel (file->fileLevel, view);
        if (!pmFPAfileOpen (file, fileView, config)) {
            psError(PS_ERR_IO, false, "failed to open %s (%s)", file->filename, file->name);
            psFree (fileView);
            return false;
        }
        psFree (fileView);
    }

    // do we need to write out a PHU?
    if (level >= file->fileLevel) {
        if (!pmFPAfileWritePHU (file, view, config)) {
            psError(PS_ERR_IO, false, "failed to write phu for %s (%s)", file->filename, file->name);
            return false;
        }
    }

    // select a writing method
    bool status = true;
    switch (file->type) {
      case PM_FPA_FILE_IMAGE:
        status = pmFPAviewWriteFitsImage(view, file, config);
        break;
      case PM_FPA_FILE_MASK:
        status = pmFPAviewWriteFitsMask(view, file, config);
        break;
      case PM_FPA_FILE_WEIGHT:
        status = pmFPAviewWriteFitsWeight(view, file, config);
        break;
      case PM_FPA_FILE_HEADER:
        psAbort ("no HEADER write functions defined");
        break;
      case PM_FPA_FILE_FRINGE:
        status = pmFPAviewWriteFitsImage (view, file, config);
        if (status) {
            if (!pmFPAviewWriteFitsTable(view, file, "FRINGE")) {
                psError(PS_ERR_UNKNOWN, false, "Unable to write fringe data from %s.\n", file->filename);
                return false;
            }
        }
        break;
      case PM_FPA_FILE_SX:
      case PM_FPA_FILE_RAW:
      case PM_FPA_FILE_OBJ:
      case PM_FPA_FILE_CMP:
      case PM_FPA_FILE_CMF:
        status = pmFPAviewWriteObjects (view, file, config);
        break;

      case PM_FPA_FILE_PSF:
        status = pmFPAviewWritePSFmodel (view, file, config);
        break;

      case PM_FPA_FILE_JPEG:
        status = pmFPAviewWriteJPEG (view, file, config);
        break;

      case PM_FPA_FILE_KAPA:
        status = pmFPAviewWriteSourcePlot (view, file, config);
        break;

      default:
        psError(PS_ERR_IO, true, "warning: type mismatch; saw type %d (%s)", file->type, file->name);
        return false;
    }
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to write %s (%s)\n", file->filename, file->name);
        return false;
    }
    psTrace ("psModules.camera", 5, "wrote %s (%s) (%d:%d:%d)\n", file->filename, file->name, view->chip, view->cell, view->readout);
    return true;
}

bool pmFPAfileClose (pmFPAfile *file, const pmFPAview *view)
{
    PS_ASSERT_PTR_NON_NULL(file, false);
    PS_ASSERT_PTR_NON_NULL(view, false);

    // an internal file should not be sent here (should not be left on config->files)
    PS_ASSERT(file->mode != PM_FPA_MODE_INTERNAL, false);

    // skip the following states
    if (file->state & PM_FPA_STATE_INACTIVE) {
        psTrace("psModules.camera", 6, "skip close for %s, files is inactive", file->name);
        return true;
    }
    if (file->state == PM_FPA_STATE_CLOSED) {
        psTrace("psModules.camera", 6, "skip close for %s, files is closed", file->name);
        return true;
    }

    // is current level == open level?
    pmFPALevel level = pmFPAviewLevel (view);
    if (file->fileLevel != level) {
        psTrace("psModules.camera", 6, "skip closing of %s at this level %s: fileLevel is %s",
                file->name, pmFPALevelToName(level), pmFPALevelToName(file->fileLevel));
        return true;
    }

    // check if we are actually open
    bool status = true;
    switch (file->type) {
        // check the FITS types
      case PM_FPA_FILE_IMAGE:
      case PM_FPA_FILE_MASK:
      case PM_FPA_FILE_WEIGHT:
      case PM_FPA_FILE_HEADER:
      case PM_FPA_FILE_FRINGE:
      case PM_FPA_FILE_CMF:
      case PM_FPA_FILE_PSF:
        psTrace ("psModules.camera", 5, "closing %s (%s) (%d:%d:%d)\n", file->filename, file->name, view->chip, view->cell, view->readout);
        status = psFitsClose (file->fits);
        file->fits = NULL;
        file->phu = NULL;
        file->header = NULL;
        file->state = PM_FPA_STATE_CLOSED;
        break;

        // ignore the TEXT types
      case PM_FPA_FILE_SX:
      case PM_FPA_FILE_RAW:
      case PM_FPA_FILE_OBJ:
      case PM_FPA_FILE_CMP:
      case PM_FPA_FILE_JPEG:
      case PM_FPA_FILE_KAPA:
        break;

      default:
        psError(PS_ERR_IO, true, "type mismatch: %d (%s)", file->type, file->name);
        return false;
    }
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to close %s (%s) (%d:%d:%d)\n", file->filename, file->name, view->chip, view->cell, view->readout);
        return false;
    }
    return true;
}

bool pmFPAfileFreeData(pmFPAfile *file, const pmFPAview *view)
{
    PS_ASSERT_PTR_NON_NULL(file, false);
    PS_ASSERT_PTR_NON_NULL(view, false);

    // an internal file should not be sent here (should not be left on config->files)
    PS_ASSERT(file->mode != PM_FPA_MODE_INTERNAL, false);

    if (file->state & PM_FPA_STATE_INACTIVE) {
        psTrace("psModules.camera", 6, "skip free for %s, files is inactive", file->name);
        return true;
    }

    // get the current level
    pmFPALevel level = pmFPAviewLevel (view);

    // do we need to free this file?
    if (level != file->freeLevel) {
        psTrace("psModules.camera", 6, "skip free of %s at this level %s: freeLevel is %s",
                file->name, pmFPALevelToName(level), pmFPALevelToName(file->freeLevel));
        return true;
    }

    bool status = true;
    switch (file->type) {
      case PM_FPA_FILE_IMAGE:
      case PM_FPA_FILE_MASK:
      case PM_FPA_FILE_WEIGHT:
      case PM_FPA_FILE_HEADER:
      case PM_FPA_FILE_FRINGE:
        status = pmFPAviewFreeData(view, file);
        break;
      case PM_FPA_FILE_SX:
      case PM_FPA_FILE_RAW:
      case PM_FPA_FILE_OBJ:
      case PM_FPA_FILE_CMP:
      case PM_FPA_FILE_CMF:
      case PM_FPA_FILE_PSF:
        psTrace ("psModules.camera", 5, "NOT freeing %s (%s) : save for further analysis\n", file->filename, file->name);
        return true;
      case PM_FPA_FILE_JPEG:
      case PM_FPA_FILE_KAPA:
        psTrace ("psModules.camera", 5, "nothing to free for %s (%s)\n", file->filename, file->name);
        return true;
      default:
        psError(PS_ERR_IO, true, "warning: type mismatch; saw type %d", file->type);
        return false;
    }
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to read %s (%s)\n", file->filename, file->name);
        return false;
    }
    psTrace ("psModules.camera", 5, "freed %s (%s) (%d:%d:%d)\n", file->filename, file->name, view->chip, view->cell, view->readout);
    return true;
}

// open file (if not already opened).
// this function is only called only within pmFPAfileRead or pmFPAfileWrite.
bool pmFPAfileOpen (pmFPAfile *file, const pmFPAview *view, pmConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(file, false);
    PS_ASSERT_PTR_NON_NULL(view, false);

    bool status;
    char *mode = NULL;
    char *readMode = "r";
    char *writeMode = "w";

    if (file->state & PM_FPA_STATE_INACTIVE) {
        psTrace("psModules.camera", 6, "skip open for %s, files is inactive", file->name);
        return true;
    }

    if (file->state == PM_FPA_STATE_OPEN) {
        return true;
    }

    // these are programming errors
    PS_ASSERT(file->mode != PM_FPA_MODE_NONE, false);
    PS_ASSERT(file->mode != PM_FPA_MODE_INTERNAL, false);

    if (file->mode == PM_FPA_MODE_READ) {
        mode = readMode;
    }
    if (file->mode == PM_FPA_MODE_WRITE) {
        mode = writeMode;
    }
    if ((file->mode == PM_FPA_MODE_WRITE) && !file->save) {
        psTrace("psModules.camera", 6, "skip open for %s, output file not requested", file->name);
        return true;
    }

    // determine the file name, free a name allocated earlier
    psFree (file->filename);
    file->filename = pmFPAfileNameFromRule (file->filerule, file, view);
    if (file->filename == NULL) {
        psError(PS_ERR_IO, true, "Filename is NULL");
        return false;
    }

    // indirect filenames: these come from a list on the command line or elsewhere
    if (!strcasecmp (file->filename, "@FILES")) {
        char *filesrc = pmFPAfileNameFromRule (file->filesrc, file, view);
        if (filesrc == NULL) {
            psError(PS_ERR_IO, false, "error converting filesrc to name %s\n", file->filesrc);
            return false;
        }

        psFree (file->filename);
        file->filename = psMetadataLookupStr (&status, file->names, filesrc);

        if (file->filename == NULL) {
            psError(PS_ERR_IO, true, "filename lookup error (@FILES) for %s : %s\n", file->filesrc, filesrc);
            psFree (filesrc);
            return false;
        }
        // psMetadataLookupStr just returns a view, file->filename must be protected
        psMemIncrRefCounter (file->filename);
        psFree (filesrc);
    }

    // get name from detrend database
    // file->detrend->detID contains the desired -det_id detID -iteration iter string
    if (!strcasecmp (file->filename, "@DETDB")) {
        if (!file->detrend) {
            psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find information about selected detrend.");
            return false;
        }

        psString classId = NULL;        // The class identifier, to pass to pmDetrendFile
        psMetadata *menu = psMetadataLookupMetadata(&status, file->camera, "CLASSID"); // Menu of class IDs
        if (!status || !menu) {
            psWarning("Unable to find CLASSID metadata in camera configuration --- "
                      "assuming no class identifier required.");
        } else {
            const char *rule = psMetadataLookupStr(&status, menu, file->detrend->level); // Rule for class_id
            if (!status || !rule || strlen(rule) == 0) {
                psWarning("Unable to find %s in CLASSID within camera configuration --- "
                          "attempting to proceed without it.", file->detrend->level);
            } else {
                classId = pmFPAfileNameFromRule(rule, file, view);
                if (!classId) {
                    psError(PS_ERR_IO, false, "error converting CLASSID rule to name: %s\n", rule);
                    return false;
                }
            }
        }
        psTrace ("psModules.camera", 6, "looking for detrend (%s, %s)\n", file->detrend->detID, classId);
        psFree (file->filename);

        file->filename = pmDetrendFile(file->detrend->detID, classId, config);
        if (file->filename == NULL) {
            psError(PS_ERR_IO, false, "failed to find a valid detrend image for detID %s : classID %s\n", file->detrend->detID, classId);
            psFree (classId);
            return false;
        }

        psTrace ("psModules.camera", 6, "got detrend file %s\n", file->filename);
        psFree (classId);
    }

    // apply filename mangling rules (file://, path://, neb://)
    bool create = file->mode == PM_FPA_MODE_WRITE ? true : false;
    psString tmpName = pmConfigConvertFilename (file->filename, config, create);
    psFree (file->filename);
    file->filename = tmpName;

    switch (file->type) {
        // open the FITS types:
      case PM_FPA_FILE_IMAGE:
      case PM_FPA_FILE_MASK:
      case PM_FPA_FILE_WEIGHT:
      case PM_FPA_FILE_HEADER:
      case PM_FPA_FILE_FRINGE:
	// XXX note that for CMF and PSF, we do not know yet if there is actually any data to 
	// write out.  this is because these types of output files have their data stored on the
	// readout->analysis metadata structure of another (existing) fpa
      case PM_FPA_FILE_CMF:
      case PM_FPA_FILE_PSF:
        psTrace ("psModules.camera", 5, "opening %s (%s) (%d:%d:%d)\n", file->filename, file->name, view->chip, view->cell, view->readout);
        file->fits = psFitsOpen (file->filename, mode);
        if (file->fits == NULL) {
            psError(PS_ERR_IO, false, "error opening file %s\n", file->filename);
            return false;
        }
        file->state = PM_FPA_STATE_OPEN;

        // if needed, set the optional EXTWORD field based on the camera value
        if (config && config->format) {
            psMetadata *fileMenu = psMetadataLookupMetadata (NULL, config->format, "FILE");
            if (!fileMenu) {
                psError (PS_ERR_IO, true, "FILE METADATA missing from camera format %s\n", config->formatName);
                return false;
            }
            char *extword = psMetadataLookupStr (&status, fileMenu, "EXTWORD");
            if (status) {
                psFitsSetExtnameWord (file->fits, extword);
            }
        }

        // In some cases, we need to read the PHU after we've opened the file.  This happens for
        // the images supplied by the detrend database, which are only identified here (above).
        if (!pmFPAfileReadPHU (file, view, config)) {
            psError (PS_ERR_IO, true, "error reading PHU for %s (%s) (%d:%d:%d)\n", file->filename, file->name, view->chip, view->cell, view->readout);
            return false;
        }
        break;

        // defer opening TEXT types:
      case PM_FPA_FILE_SX:
      case PM_FPA_FILE_OBJ:
      case PM_FPA_FILE_CMP:
      case PM_FPA_FILE_RAW:
      case PM_FPA_FILE_JPEG:
      case PM_FPA_FILE_KAPA:
        psTrace ("psModules.camera", 5, "defer opening %s\n", file->filename);
        break;

      default:
        psError(PS_ERR_IO, true, "type mismatch for %s : %d\n", file->filename, file->type);
        return false;
    }
    return true;
}

// for this file and view, if we need to read a PHU, read it.  return true for any non-error
// condition. this function should be called by pmFPAfileOpen.
bool pmFPAfileReadPHU (pmFPAfile *file, const pmFPAview *view, pmConfig *config)
{
    // required conditions
    if (file->mode != PM_FPA_MODE_READ) return true;
    if (file->state != PM_FPA_STATE_OPEN) psAbort ("pmFPAfileReadPHU called on unopened file");
    if (file->fpa == NULL) psAbort ("pmFPAfileReadPHU called on file without an FPA");

    // check if we need to read a PHU (if not, return true)
    switch (file->fileLevel) {
      case PM_FPA_LEVEL_FPA:
        if (file->fpa->hdu) return true;
        break;
      case PM_FPA_LEVEL_CHIP: {
          pmChip *chip = pmFPAviewThisChip(view, file->fpa);
          if (!chip) psAbort ("inconsistent file/fpa: fileLevel is CHIP, view is FPA");
          if (chip->hdu) return true;
          break;
      }
      case PM_FPA_LEVEL_CELL: {
          pmCell *cell = pmFPAviewThisCell(view, file->fpa);
          if (!cell) psAbort ("inconsistent file/fpa: fileLevel is CELL, view is FPA");
          if (cell->hdu) return true;
          break;
      }
      case PM_FPA_LEVEL_NONE:
        // Might get here immediately after opening a file selected from the detrend database.
        break;
      default:
        psAbort("fileLevel not correctly set");
        break;
    }

    // XXX do we need to advance to the first HDU?
    psMetadata *phu = psFitsReadHeader (NULL, file->fits);
    if (!file->format) {
        // XXX do we need to read the recipe here?  these files are supplemental, and probably
        // do not need to re-load the recipes
        file->format = pmConfigCameraFormatFromHeader (config, phu, false);
        if (!file->format) {
            psError(PS_ERR_IO, false, "Failed to read CCD format from %s\n", file->filename);
            psFree(phu);
            return false;
        }
    } else {
        bool valid;
        if (!pmConfigValidateCameraFormat (&valid, file->format, phu)) {
            psError (PS_ERR_UNKNOWN, false, "Error in camera configuration\n");
            psFree (phu);
            return false;
        }
        if (!valid) {
            psError(PS_ERR_IO, false, "file %s is not from the required camera", file->filename);
            psFree (phu);
            return false;
        }
    }
    pmFPAview *thisView = pmFPAAddSourceFromHeader (file->fpa, phu, file->format);
    psFree (thisView);
    psFree (phu);
    // XXX we can check the output view to be sure it corresponds to our current view
    return true;
}

// XXX this function is only called from pmFPAfileWrite
// XXX for each data type, there should be a function which writes the PHU, if needed
bool pmFPAfileWritePHU(pmFPAfile *file, const pmFPAview *view, const pmConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(file, false);
    PS_ASSERT_PTR_NON_NULL(view, false);

    bool status = true;
    switch (file->type) {
      case PM_FPA_FILE_IMAGE:
      case PM_FPA_FILE_MASK:
      case PM_FPA_FILE_WEIGHT:
      case PM_FPA_FILE_FRINGE:
        if (file->mode == PM_FPA_MODE_WRITE) {
            // Want to write out any potential blank
            // XXX why do we need to construct a new fpa??
            pmFPA *fpa = pmFPAfileSuitableFPA(file, view, config); // FPA, ensuring it's ready to write a blank

            switch (file->fileLevel) {
              case PM_FPA_LEVEL_FPA:
                if (file->fpa->wrote_phu) break;
                status = pmFPAWrite(fpa, file->fits, NULL, true, false);
                file->fpa->wrote_phu = true;
                break;
              case PM_FPA_LEVEL_CHIP: {
                  pmChip *chip = pmFPAviewThisChip(view, fpa);
                  if (chip->wrote_phu) break;
                  status = pmChipWrite(chip, file->fits, NULL, true, false);
                  chip->wrote_phu = true;
                  break;
              }
              case PM_FPA_LEVEL_CELL: {
                  pmCell *cell = pmFPAviewThisCell(view, fpa);
                  if (cell->wrote_phu) break;
                  status = pmCellWrite(cell, file->fits, NULL, true);
                  cell->wrote_phu = true;
                  break;
              }
              default:
                psAbort("fileLevel not correctly set");
                break;
            }

            psFree(fpa);
        }
      case PM_FPA_FILE_SX:
      case PM_FPA_FILE_RAW:
      case PM_FPA_FILE_OBJ:
      case PM_FPA_FILE_CMP:
      case PM_FPA_FILE_CMF:
      case PM_FPA_FILE_PSF:
      case PM_FPA_FILE_JPEG:
      case PM_FPA_FILE_KAPA:
        break;
      default:
        fprintf (stderr, "warning: type mismatch\n");
        return false;
    }
    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to write PHU for %s (%s)\n", file->filename, file->name);
        return false;
    }
    return true;
}

// set the state of the specified pmFPAfile to active (state == true) or inactive
// if name is NULL, set the state for all pmFPAfiles
bool pmFPAfileActivate (psMetadata *files, bool state, char *name)
{
    PS_ASSERT_PTR_NON_NULL(files, false);

    if (!name) {
        psMetadataItem *item = NULL;
        psMetadataIterator *iter = psMetadataIteratorAlloc (files, PS_LIST_HEAD, NULL);
        while ((item = psMetadataGetAndIncrement (iter)) != NULL) {
            pmFPAfile *file = item->data.V;
            if (state) {
                file->state &= PS_NOT_U8(PM_FPA_STATE_INACTIVE);
            } else {
                file->state |= PM_FPA_STATE_INACTIVE;
            }
        }
        psFree (iter);
        return true;
    }

    bool status = false;
    pmFPAfile *file = psMetadataLookupPtr (&status, files, name);
    if (!status) {
        psTrace("psModules.camera", 6, "%s is not a defined IO file", name);
        return false;
    }
    if (!file) {
        psError(PS_ERR_IO, true, "file %s is NULL", name);
        return false;
    }
    if (state) {
        file->state &= PS_NOT_U8(PM_FPA_STATE_INACTIVE);
    } else {
        file->state |= PM_FPA_STATE_INACTIVE;
    }
    return true;
}
