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

# include "ppImage.h"

ppImageOptions *ppImageParseCamera(pmConfig *config)
{
    bool status = false;

    // the input image defines the camera, and all recipes and options the follow
    pmFPAfile *input = pmFPAfileDefineFromArgs (&status, config, "PPIMAGE.INPUT", "INPUT");
    if (!status || !input) {
        psError(PS_ERR_IO, false, "Failed to build FPA from PPIMAGE.INPUT");
        return NULL;
    }
    if (input->type != PM_FPA_FILE_IMAGE) {
        psError(PS_ERR_IO, true, "PPIMAGE.INPUT is not of type IMAGE");
        return NULL;
    }

    // if MASK or WEIGHT was supplied on command line, bind files to 'input'.
    // the mask and weight will be mosaicked with the image
    pmFPAfile *inputMask = pmFPAfileBindFromArgs(&status, input, config, "PPIMAGE.INPUT.MASK", "PPIMAGE.INPUT.MASK");
    if (!status) {
        psError (PS_ERR_UNKNOWN, false, "failed to load find definition");
        return NULL;
    }
    if (inputMask) {
      if (inputMask->type != PM_FPA_FILE_MASK) {
        psError(PS_ERR_IO, true, "PPIMAGE.INPUT.MASK is not of type MASK");
        return NULL;
      }
    }

    pmFPAfile *inputWeight = pmFPAfileBindFromArgs(&status, input, config, "PPIMAGE.INPUT.WEIGHT", "PPIMAGE.INPUT.WEIGHT");
    if (!status) {
        psError (PS_ERR_UNKNOWN, false, "failed to load find definition");
        return NULL;
    }
    if (inputWeight && inputWeight->type != PM_FPA_FILE_WEIGHT) {
        psError(PS_ERR_IO, true, "PPIMAGE.INPUT.WEIGHT is not of type WEIGHT");
        return NULL;
    }

    // add recipe options supplied on command line
    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, RECIPE_NAME);

    // parse the options from the metadata format to the ppImageOptions structure
    ppImageOptions *options = ppImageOptionsParse (config);

    // the following are defined from the argument list, if given,
    // otherwise they revert to the config information
    // not all input or output images are used in a given recipe
    if (options->doBias) {
        if (!ppImageDefineFile (config, input->fpa, "PPIMAGE.BIAS", "BIAS", PM_FPA_FILE_IMAGE, PM_DETREND_TYPE_BIAS)) {
            psError (PS_ERR_IO, false, "Can't find a bias image source");
            psFree (options);
            return NULL;
        }
    }
    if (options->doDark) {
        if (!ppImageDefineFile (config, input->fpa, "PPIMAGE.DARK", "DARK", PM_FPA_FILE_DARK, PM_DETREND_TYPE_DARK)) {
            psError (PS_ERR_IO, false, "Can't find a dark image source");
            psFree (options);
            return NULL;
        }
    }
    if (options->doMask) {

        if (!ppImageDefineFile (config, input->fpa, "PPIMAGE.MASK", "MASK", PM_FPA_FILE_MASK, PM_DETREND_TYPE_MASK)) {
            psError (PS_ERR_IO, false, "Can't find a mask image source");
            psFree (options);
            return NULL;
        }
        // XXX have ppImageDefineFile return the pmFPAfile?
        pmFPAfile *mask = psMetadataLookupPtr(&status, config->files, "PPIMAGE.MASK");
        psAssert (mask, "mask not defined?  not possible!");

        // Need to read the names of bit masks from the mask header and set them in the
        // recipe.  If we are loading this from the detrend db, this action will happen
        // when the file is resolved.
        if (!mask->detrend) {
            // XXX need to load the mask bit names from one of the headers
            // this grabs the first available hdu : no guarantee that it will be valid, though
            pmHDU *hdu = pmHDUGetFirst (mask->fpa);
            if (!hdu) {
                psError(PS_ERR_IO, true, "no valid HDU for PPIMAGE.INPUT.MASK");
                return NULL;
            }
            // XXX is this consistent with the pmConfigMaskReadHeader call above?
            if (!pmConfigMaskReadHeader (config, hdu->header)) {
                psError(PS_ERR_IO, false, "error in mask bits");
                return NULL;
            }
        }
    }
    if (options->doShutter) {
        if (!ppImageDefineFile (config, input->fpa, "PPIMAGE.SHUTTER", "SHUTTER", PM_FPA_FILE_IMAGE, PM_DETREND_TYPE_SHUTTER)) {
            psError (PS_ERR_IO, false, "Can't find a shutter image source");
            psFree (options);
            return NULL;
        }
    }

    if (options->doFlat) {
        if (!ppImageDefineFile (config, input->fpa, "PPIMAGE.FLAT", "FLAT", PM_FPA_FILE_IMAGE, PM_DETREND_TYPE_FLAT)) {
            psError (PS_ERR_IO, false, "Can't find a flat image source");
            psFree (options);
            return NULL;
        }
    }

    int nThreads = psMetadataLookupS32 (&status, config->arguments, "NTHREADS");
    if (nThreads > 0) {
        int nScanRows = psMetadataLookupS32 (&status, recipe, "SCAN.ROWS");
        pmDetrendSetThreadTasks (nScanRows);
    }

    // fringe frame are only applied for a subset of the filters.
    // if the filter is one of those identified by a FRINGE.FILTERS metadata entry
    // in ppImage.config, apply the fringe frame
    if (options->doFringe) {
        // determine filter from the concepts
        const char *filter = psMetadataLookupStr(&status, input->fpa->concepts, "FPA.FILTER");
        if (!status || !filter || strlen(filter) == 0) {
            psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find FPA.FILTER.\n");
            psFree(options);
            return NULL;
        }

        // select FRINGE.FILTERS from the recipe and test if the filter matches
        // is the mdi saved with psList or string?
        psMetadataItem *mdi = psMetadataLookup (recipe, "FRINGE.FILTERS");
        if (mdi == NULL) {
            // no valid filters for fringe data for this camera
            options->doFringe = false;
            goto skip_fringe;
        }
        // place entry on a list regardless of type
        psList *filters = NULL;
        if (mdi->type == PS_DATA_STRING) {
            filters = psListAlloc(NULL);
            psListAdd (filters, PS_LIST_HEAD, mdi);
        } else if (mdi->type == PS_DATA_METADATA_MULTI) {
            filters = psMemIncrRefCounter(mdi->data.list);
        } else {
            psError(PS_ERR_BAD_PARAMETER_TYPE, true,
                    "FRINGE.FILTERS in recipe %s is not of type METADATA", RECIPE_NAME);
            psFree(options);
            return NULL;
        }

        // search through list to find the current filter
        psListIterator *iter = psListIteratorAlloc (filters, PS_LIST_HEAD, FALSE);
        options->doFringe = false;
        for (int i = 0; !options->doFringe && (i < filters->n); i++) {
            psMetadataItem *item = psListGetAndIncrement (iter);
            char *validFilter = item->data.V;
            if (strcmp (validFilter, filter)) continue;
            options->doFringe = true;
        }
        psFree(iter);
        psFree(filters);
    }
skip_fringe:
    if (options->doFringe) {
        if (!ppImageDefineFile (config, input->fpa, "PPIMAGE.FRINGE", "FRINGE", PM_FPA_FILE_FRINGE, PM_DETREND_TYPE_FRINGE)) {
            psError (PS_ERR_IO, false, "Can't find a fringe image source");
            return NULL;
        }
    }

    // the following files are output targets
    pmFPAfile *outImage = pmFPAfileDefineOutput(config, input->fpa, "PPIMAGE.OUTPUT");
    if (!outImage) {
        psError(PS_ERR_IO, false, _("Unable to generate output file from PPIMAGE.OUTPUT"));
        psFree(options);
        return NULL;
    }
    if (outImage->type != PM_FPA_FILE_IMAGE) {
        psError(PS_ERR_IO, true, "PPIMAGE.OUTPUT is not of type IMAGE");
        psFree(options);
        return NULL;
    }
    pmFPAfile *outMask = pmFPAfileDefineOutput(config, input->fpa, "PPIMAGE.OUTPUT.MASK");
    if (!outMask) {
        psError(PS_ERR_IO, false, _("Unable to generate output file from PPIMAGE.OUTPUT.MASK"));
        psFree(options);
        return NULL;
    }
    if (outMask->type != PM_FPA_FILE_MASK) {
        psError(PS_ERR_IO, true, "PPIMAGE.OUTPUT.MASK is not of type MASK");
        psFree(options);
        return NULL;
    }
    pmFPAfile *outWeight = pmFPAfileDefineOutput(config, input->fpa, "PPIMAGE.OUTPUT.WEIGHT");
    if (!outWeight) {
        psError(PS_ERR_IO, false, _("Unable to generate output file from PPIMAGE.OUTPUT.WEIGHT"));
        psFree(options);
        return NULL;
    }
    if (outWeight->type != PM_FPA_FILE_WEIGHT) {
        psError(PS_ERR_IO, true, "PPIMAGE.OUTPUT.WEIGHT is not of type WEIGHT");
        psFree(options);
        return NULL;
    }

    // XXX should these be bound explicitly to the outImage->fpa rather than the input->fpa?
    pmFPAfile *chipImage = pmFPAfileDefineChipMosaic(config, input->fpa, "PPIMAGE.CHIP");
    if (!chipImage) {
        psError(PS_ERR_IO, false, _("Unable to generate new file from PPIMAGE.CHIP"));
        psFree(options);
        return NULL;
    }
    if (chipImage->type != PM_FPA_FILE_IMAGE) {
        psError(PS_ERR_IO, true, "PPIMAGE.CHIP is not of type IMAGE");
        psFree(options);
        return NULL;
    }
    pmFPAfile *chipMask = pmFPAfileDefineOutput(config, chipImage->fpa, "PPIMAGE.CHIP.MASK");
    if (!chipMask) {
        psError(PS_ERR_IO, false, _("Unable to generate output file from PPIMAGE.CHIP.MASK"));
        psFree(options);
        return NULL;
    }
    if (chipMask->type != PM_FPA_FILE_MASK) {
        psError(PS_ERR_IO, true, "PPIMAGE.CHIP.MASK is not of type MASK");
        psFree(options);
        return NULL;
    }
    pmFPAfile *chipWeight = pmFPAfileDefineOutput(config, chipImage->fpa, "PPIMAGE.CHIP.WEIGHT");
    if (!chipWeight) {
        psError(PS_ERR_IO, false, _("Unable to generate output file from PPIMAGE.CHIP.WEIGHT"));
        psFree(options);
        return NULL;
    }
    if (chipWeight->type != PM_FPA_FILE_WEIGHT) {
        psError(PS_ERR_IO, true, "PPIMAGE.CHIP.WEIGHT is not of type WEIGHT");
        psFree(options);
        return NULL;
    }

    pmFPAfile *byFPA1 = pmFPAfileDefineFPAMosaic(config, input->fpa, "PPIMAGE.OUTPUT.FPA1");
    if (!byFPA1) {
        psError(PS_ERR_IO, false, _("Unable to generate new file from PPIMAGE.OUTPUT.FPA1"));
        psFree(options);
        return NULL;
    }
    if (byFPA1->type != PM_FPA_FILE_IMAGE) {
        psError(PS_ERR_IO, true, "PPIMAGE.OUTPUT.FPA1 is not of type IMAGE");
        psFree(options);
        return NULL;
    }
    pmFPAfile *byFPA2 = pmFPAfileDefineFPAMosaic(config, input->fpa, "PPIMAGE.OUTPUT.FPA2");
    if (!byFPA2) {
        psError(PS_ERR_IO, false, _("Unable to generate new file from PPIMAGE.OUTPUT.FPA2"));
        psFree(options);
        return NULL;
    }
    if (byFPA2->type != PM_FPA_FILE_IMAGE) {
        psError(PS_ERR_IO, true, "PPIMAGE.OUTPUT.FPA2 is not of type IMAGE");
        psFree(options);
        return NULL;
    }

    // chipImage    -> psphotInput  (pmFPAfileDefineFromFile)       : fpa is constructed
    // psphotInput  -> psphotOutput (pmFPAfileDefineOutputFromFile) : fpa is equated
    // psphotOutput -> psastroInput (pmFPAfileDefineInput)          : fpa is ref-copied
    // psastroInput -> psastroModel (pmFPAfileDefineFromArgs        : fpa is ref-copied
    // psastroInput -> psastroModel (pmFPAfileDefineFromConf        : fpa is constructed
    // psastroInput -> psastroModel (pmFPAfileDefineFromDetDB       : fpa is constructed (pmDetrendSelect uses input concepts )

    // For photometry, we operate on the chip-mosaicked image
    // we create a copy of the mosaicked image for psphot so we can write out a clean image
    if (options->doPhotom || options->doBG) {

        // this file is just used as a carrier; output files (eg, PSPHOT.RESID) are defined by
        // psphotDefineFiles
        pmFPAfile *psphotInput = pmFPAfileDefineFromFile (config, chipImage, 1, 1, "PSPHOT.INPUT");
        PS_ASSERT (psphotInput, false);

        // define associated psphot input/output files
        if (!psphotDefineFiles (config, psphotInput)) {
            psError(PSPHOT_ERR_CONFIG, false, "Trouble defining the additional input/output files for psphot");
            return false;
        }
    }

    // For photometry, we operate on the chip-mosaicked image
    if (options->doAstromChip || options->doAstromMosaic) {
        if (!options->doPhotom) {
            psError (PSASTRO_ERR_CONFIG, false, "photometry mode is not selected; it is required for astrometry");
            return false;
        }

        pmFPAfile *psphotOutput = psMetadataLookupPtr (&status, config->files, "PSPHOT.OUTPUT");
        PS_ASSERT (psphotOutput, false);

        pmFPAfile *psastroInput = pmFPAfileDefineInput (config, psphotOutput->fpa, NULL, "PSASTRO.INPUT");
        PS_ASSERT (psastroInput, false);
        psastroInput->mode = PM_FPA_MODE_REFERENCE;

        // define associated psphot input/output files
        if (!psastroDefineFiles (config, psastroInput)) {
            psError(PSPHOT_ERR_CONFIG, false, "Trouble defining the additional input/output files for psastro");
            return false;
        }

        // deactivate the psastro files, reactive when needed
        pmFPAfileActivate (config->files, false, "PSASTRO.OUTPUT");
    }

    // save any of these files?
    outImage->save   = options->BaseFITS;
    outMask->save    = options->BaseMaskFITS;
    outWeight->save  = options->BaseWeightFITS;

    chipImage->save  = options->ChipFITS;
    chipMask->save   = options->ChipMaskFITS;
    chipWeight->save = options->ChipWeightFITS;

    byFPA1->save     = options->FPA1FITS;
    byFPA2->save     = options->FPA2FITS;

    // outImage is used as a carrier: input to chipImage -> require the data to remain at the CHIP level
    outImage->freeLevel = PS_MIN (outImage->freeLevel, PM_FPA_LEVEL_CHIP);
    outImage->dataLevel = outImage->freeLevel;
    outImage->fileLevel = PS_MIN (outImage->fileLevel, outImage->dataLevel);

    // outMask and outWeight must be freed at the same level as outImage (all freed by pmFPAFreeData)
    outMask->freeLevel   = outImage->freeLevel;
    outWeight->freeLevel = outImage->freeLevel;
    outMask->dataLevel   = outImage->dataLevel;
    outWeight->dataLevel = outImage->dataLevel;

    // Ditto for the chip-mosaicked version
    chipMask->freeLevel   = chipImage->freeLevel;
    chipWeight->freeLevel = chipImage->freeLevel;
    chipMask->dataLevel   = chipImage->dataLevel;
    chipWeight->dataLevel = chipImage->dataLevel;

    // the input data is the same as the outImage data : force the free levels to match
    input->freeLevel = PS_MIN (outImage->freeLevel, input->freeLevel);

    // define the binned target files (which may just be carriers for some camera configurations)
    pmFPAfile *bin1 = pmFPAfileDefineFromFPA (config, chipImage->fpa, options->xBin1, options->yBin1, "PPIMAGE.BIN1");
    if (!bin1) {
        psError(PS_ERR_IO, false, _("Unable to generate new file from PPIMAGE.BIN1"));
        psFree(options);
        return NULL;
    }
    if (bin1->type != PM_FPA_FILE_IMAGE) {
        psError(PS_ERR_IO, true, "PPIMAGE.OUTPUT.BIN1 is not of type IMAGE");
        psFree(options);
        return NULL;
    }

    pmFPAfile *bin2 = pmFPAfileDefineFromFPA (config, chipImage->fpa, options->xBin2, options->yBin2, "PPIMAGE.BIN2");
    if (!bin2) {
        psError(PS_ERR_IO, false, _("Unable to generate new file from PPIMAGE.BIN2"));
        psFree(options);
        return NULL;
    }
    if (bin2->type != PM_FPA_FILE_IMAGE) {
        psError(PS_ERR_IO, true, "PPIMAGE.OUTPUT.BIN2 is not of type IMAGE");
        psFree(options);
        return NULL;
    }

    // bin1 and bin2 are used as carriers: input for byFPA1, byFPA2
    bin1->freeLevel = PM_FPA_LEVEL_FPA;
    bin2->freeLevel = PM_FPA_LEVEL_FPA;

    pmFPAfile *jpg1 = pmFPAfileDefineOutput (config, byFPA1->fpa, "PPIMAGE.JPEG1");
    if (!jpg1) {
        psError(PS_ERR_IO, false, _("Unable to generate new file from PPIMAGE.JPEG1"));
        psFree(options);
        return NULL;
    }
    if (jpg1->type != PM_FPA_FILE_JPEG) {
        psError(PS_ERR_IO, true, "PPIMAGE.OUTPUT.JPEG1 is not of type JPEG");
        psFree(options);
        return NULL;
    }
    pmFPAfile *jpg2 = pmFPAfileDefineOutput (config, byFPA2->fpa, "PPIMAGE.JPEG2");
    if (!jpg2) {
        psError(PS_ERR_IO, false, _("Unable to generate new file from PPIMAGE.JPEG2"));
        psFree(options);
        return NULL;
    }
    if (jpg2->type != PM_FPA_FILE_JPEG) {
        psError(PS_ERR_IO, true, "PPIMAGE.OUTPUT.JPEG2 is not of type JPEG");
        psFree(options);
        return NULL;
    }

    // XXX we could potentially not define these pmFPAfiles if no output is requested...
    bin1->save = options->Bin1FITS;
    bin2->save = options->Bin2FITS;
    jpg1->save = options->Bin1JPEG;
    jpg2->save = options->Bin2JPEG;

    // Chip selection: turn on only the chips specified (pass status to suppress missing-key log msg)
    char *chipLine = psMetadataLookupStr(&status, config->arguments, "CHIP_SELECTIONS");
    psArray *chips = psStringSplitArray (chipLine, ",", false);
    if (chips->n > 0) {
        pmFPASelectChip (input->fpa, -1, true); // deselect all chips
        for (int i = 0; i < chips->n; i++) {
            int chipNum = atoi(chips->data[i]);
            if (! pmFPASelectChip(input->fpa, chipNum, false)) {
                psError(PS_ERR_IO, false, "Chip number %d doesn't exist in camera.\n", chipNum);
                psFree(options);
                return false;
            }
        }
    }
    psFree (chips);

    if (psMetadataLookupBool(NULL, config->arguments, "INPUT_IS_FRINGE")) {
        // It's a fringe file, so change the file type
        input->type = PM_FPA_FILE_FRINGE;
        outImage->type = PM_FPA_FILE_FRINGE;
    }
    if (psMetadataLookupBool(NULL, config->arguments, "INPUT_IS_DARK")) {
        // It's a dark file, so change the file type
        input->type = PM_FPA_FILE_DARK;
        outImage->type = PM_FPA_FILE_DARK;
        // Turn off compression --- there are just too many nasties that can happen
        psFree(outImage->compression);
        outImage->compression = NULL;
        psFree(outImage->options);
        outImage->options = NULL;
    }

    // Turn off mask and weight output if we're not doing anything interesting
    if (!options->doMaskBuild && outMask->save) {
        psWarning("output mask image (BASE.MASK.FITS) requested, but not generated: skipping.\n");
        outMask->save = false;
    }
    if (!options->doWeightBuild && outWeight->save) {
        psWarning("output weight image (BASE.WEIGHT.FITS) requested, but not generated: skipping.\n");
        outWeight->save = false;
    }
    if (!options->doMaskBuild && chipMask->save) {
        psWarning("output mask image (CHIP.MASK.FITS) requested, but not generated: skipping.\n");
        chipMask->save = false;
    }
    if (!options->doWeightBuild && chipWeight->save) {
        psWarning("output weight image (CHIP.WEIGHT.FITS) requested, but not generated: skipping.\n");
        chipWeight->save = false;
    }

    if (psTraceGetLevel("ppImage.config") > 0) {
        // Get a look inside all the files.
        psMetadataIterator *filesIter = psMetadataIteratorAlloc(config->files, PS_LIST_HEAD, NULL); // Iterator
        psMetadataItem *item;               // Item from iteration
        fprintf(stderr, "Files:\n");
        while ((item = psMetadataGetAndIncrement(filesIter))) {
            pmFPAfile *file = item->data.V; // File of interest
            fprintf(stderr, "%s: %p %p %p (%p) %p\n", file->name,
                    file->src, file->fpa,
                    file->camera, file->fpa->camera, file->format);
        }
        psFree(filesIter);
    }

    // Change the input dark type between the old (IMAGE) and new (multi-DARK).
    // Hopefully this is a temporary change until we all move over to using the new dark types
    bool mdok;                          // Status of MD lookup
    if (options->doDark && psMetadataLookupBool(&mdok, recipe, "OLDDARK")) {
        pmFPAfile *dark = psMetadataLookupPtr(NULL, config->files, "PPIMAGE.DARK");
        assert(dark);
        dark->type = PM_FPA_FILE_IMAGE;
    }

    return (options);
}
