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

#include <stdio.h>
#include <string.h>
#include <pslib.h>
#include <psmodules.h>
#include <psphot.h>

#include "ppStack.h"

bool ppStackCamera(pmConfig *config)
{
    bool haveWeights = false;           // Do we have weight maps?

    psMetadata *inputs = psMetadataLookupMetadata(NULL, config->arguments, "INPUTS"); // The inputs info
    psMetadataIterator *iter = psMetadataIteratorAlloc(inputs, PS_LIST_HEAD, NULL); // Iterator
    psMetadataItem *item;               // Item from iteration
    int i = 0;                          // Counter
    while ((item = psMetadataGetAndIncrement(iter))) {
        if (item->type != PS_DATA_METADATA) {
            psError(PS_ERR_BAD_PARAMETER_TYPE, true,
                    "Component %s of the input metadata is not of type METADATA", item->name);
            psFree(iter);
            return false;
        }

        psMetadata *input = item->data.md; // The input metadata of interest

        psString image = psMetadataLookupStr(NULL, input, "IMAGE"); // Name of image
        if (!image || strlen(image) == 0) {
            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Component %s lacks IMAGE of type STR", item->name);
            psFree(iter);
            return false;
        }

        bool mdok;
        psString mask = psMetadataLookupStr(&mdok, input, "MASK"); // Name of mask
        psString weight = psMetadataLookupStr(&mdok, input, "WEIGHT"); // Name of weight map

        float weighting = psMetadataLookupF32(&mdok, input, "WEIGHTING"); // Relative weighting
        if (!mdok || isnan(weighting) || weighting == 0.0) {
            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Component %s lacks WEIGHTING of type F32", item->name);
            psFree(iter);
            return false;
        }

        float scale = psMetadataLookupF32(&mdok, input, "SCALE"); // Relative scale
        if (!mdok || isnan(scale) || scale == 0.0) {
            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Component %s lacks SCALE of type F32", item->name);
            psFree(iter);
            return false;
        }

        // Add the image file
        psArray *imageFiles = psArrayAlloc(1); // Array of filenames for this FPA
        imageFiles->data[0] = psMemIncrRefCounter(image);
        psMetadataAddArray(config->arguments, PS_LIST_TAIL, "IMAGE.FILENAMES", PS_META_REPLACE,
                           "Filenames of image files", imageFiles);
        psFree(imageFiles);

        bool found = false;             // Found the file?
        pmFPAfile *imageFile = pmFPAfileDefineFromArgs(&found, config, "PPSTACK.INPUT", "IMAGE.FILENAMES");
        if (!imageFile || !found) {
            psError(PS_ERR_UNKNOWN, false, "Unable to define file from image %d (%s)", i, image);
            return false;
        }
        if (imageFile->type != PM_FPA_FILE_IMAGE) {
            psError(PS_ERR_IO, true, "PPSTACK.INPUT is not of type IMAGE");
            return false;
        }

        // Optionally add the mask file
        if (mask && strlen(mask) > 0) {
            psArray *maskFiles = psArrayAlloc(1); // Array of filenames for this FPA
            maskFiles->data[0] = psMemIncrRefCounter(mask);
            psMetadataAddArray(config->arguments, PS_LIST_TAIL, "MASK.FILENAMES", PS_META_REPLACE,
                               "Filenames of mask files", maskFiles);
            psFree(maskFiles);

            bool status;
            pmFPAfile *maskFile = pmFPAfileBindFromArgs(&status, imageFile, config, "PPSTACK.INPUT.MASK",
                                                        "MASK.FILENAMES");
            if (!status) {
                psError(PS_ERR_UNKNOWN, false, "Unable to define file from mask %d (%s)", i, mask);
                return false;
            }
            if (maskFile->type != PM_FPA_FILE_MASK) {
                psError(PS_ERR_IO, true, "PPSTACK.INPUT.MASK is not of type MASK");
                return false;
            }
        }

        // Optionally add the weight file
        if (weight && strlen(weight) > 0) {
            haveWeights = true;
            psArray *weightFiles = psArrayAlloc(1); // Array of filenames for this FPA
            weightFiles->data[0] = psMemIncrRefCounter(weight);
            psMetadataAddArray(config->arguments, PS_LIST_TAIL, "WEIGHT.FILENAMES", PS_META_REPLACE,
                               "Filenames of weight files", weightFiles);
            psFree(weightFiles);

            bool status;
            pmFPAfile *weightFile = pmFPAfileBindFromArgs(&status, imageFile, config, "PPSTACK.INPUT.WEIGHT",
                                                          "WEIGHT.FILENAMES");
            if (!status) {
                psError(PS_ERR_UNKNOWN, false, "Unable to define file from weight %d (%s)", i, weight);
                return false;
            }
            if (weightFile->type != PM_FPA_FILE_WEIGHT) {
                psError(PS_ERR_IO, true, "PPSTACK.INPUT.WEIGHT is not of type WEIGHT");
                return false;
            }
        }

        psMetadataAddF32(imageFile->fpa->analysis, PS_LIST_TAIL, "PPSTACK.WEIGHTING", 0,
                         "Relative weighting for image", weighting);
        psMetadataAddF32(imageFile->fpa->analysis, PS_LIST_TAIL, "PPSTACK.SCALE", 0,
                         "Relative scale for image", scale);

        i++;
    }
    psFree(iter);
    psMetadataRemoveKey(config->arguments, "IMAGE.FILENAMES");
    if (psMetadataLookup(config->arguments, "MASK.FILENAMES")) {
        psMetadataRemoveKey(config->arguments, "MASK.FILENAMES");
    }
    if (psMetadataLookup(config->arguments, "WEIGHT.FILENAMES")) {
        psMetadataRemoveKey(config->arguments, "WEIGHT.FILENAMES");
    }

    psMetadataAddS32(config->arguments, PS_LIST_TAIL, "INPUTS.NUM", 0, "Number of input files", i);

    // Output image
    pmFPA *fpa = pmFPAConstruct(config->camera); // FPA to contain the output
    if (!fpa) {
        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to construct an FPA from camera configuration.");
        return false;
    }
    pmFPAfile *output = pmFPAfileDefineOutput(config, fpa, "PPSTACK.OUTPUT");
    psFree(fpa);                        // Drop reference
    if (!output) {
        psError(PS_ERR_IO, false, _("Unable to generate output file from PPSTACK.OUTPUT"));
        return false;
    }
    if (output->type != PM_FPA_FILE_IMAGE) {
        psError(PS_ERR_IO, true, "PPSTACK.OUTPUT is not of type IMAGE");
        return false;
    }
    output->save = true;

    // Generate the correct structure
    pmFPALevel phuLevel = pmFPAPHULevel(output->format); // Level at which PHU goes
    pmFPAview *view = pmFPAviewAlloc(0);// View for current level
    if (phuLevel == PM_FPA_LEVEL_FPA) {
        if (!pmFPAAddSourceFromView(fpa, "Stack", view, output->format)) {
            psError(PS_ERR_UNKNOWN, false, "Unable to add PHU to FPA.");
            psFree(fpa);
            psFree(view);
            return NULL;
        }
    } else {
        pmChip *chip;                       // Chip from FPA
        while ((chip = pmFPAviewNextChip(view, fpa, 1))) {
            if (phuLevel == PM_FPA_LEVEL_CHIP) {
                if (!pmFPAAddSourceFromView(fpa, "Stack", view, output->format)) {
                    psError(PS_ERR_UNKNOWN, false, "Unable to add PHU to FPA.");
                    psFree(fpa);
                    psFree(view);
                    return NULL;
                }
            } else {
                pmCell *cell;                   // Cell from chip
                while ((cell = pmFPAviewNextCell(view, fpa, 1))) {
                    if (phuLevel == PM_FPA_LEVEL_CELL) {
                        if (!pmFPAAddSourceFromView(fpa, "Stack", view, output->format)) {
                            psError(PS_ERR_UNKNOWN, false, "Unable to add PHU to FPA.");
                            psFree(fpa);
                            psFree(view);
                            return NULL;
                        }
                    }
                }
            }
        }
    }
    psFree(view);

    // Output mask
    pmFPAfile *outMask = pmFPAfileDefineOutput(config, output->fpa, "PPSTACK.OUTPUT.MASK");
    if (!outMask) {
        psError(PS_ERR_IO, false, _("Unable to generate output file from PPSTACK.OUTPUT.MASK"));
        return false;
    }
    if (outMask->type != PM_FPA_FILE_MASK) {
        psError(PS_ERR_IO, true, "PPSTACK.OUTPUT.MASK is not of type MASK");
        return false;
    }
    outMask->save = true;

    // Output weight
    if (haveWeights) {
        pmFPAfile *outWeight = pmFPAfileDefineOutput(config, output->fpa, "PPSTACK.OUTPUT.WEIGHT");
        if (!outWeight) {
            psError(PS_ERR_IO, false, _("Unable to generate output file from PPSTACK.OUTPUT.WEIGHT"));
            return false;
        }
        if (outWeight->type != PM_FPA_FILE_WEIGHT) {
            psError(PS_ERR_IO, true, "PPSTACK.OUTPUT.WEIGHT is not of type WEIGHT");
            return false;
        }
        outWeight->save = true;
    }

    // Sources for use as stamps
    bool status = false;                // Found the file?
    pmFPAfile *sources = pmFPAfileDefineFromArgs(&status, config, "PPSTACK.SOURCES", "PPSTACK.SOURCES");
    if (!status) {
        psError(PS_ERR_IO, false, "Failed to load file definition PPSTACK.SOURCES");
        return false;
    }
    if (sources && sources->type != PM_FPA_FILE_CMF) {
        psError(PS_ERR_IO, true, "PPSTACK.SOURCES is not of type CMF");
        return false;
    }

    // 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
    bool mdok = false;
    bool doPhotom = psMetadataLookupBool(&mdok, config->arguments, "PHOTOMETRY"); // perform photometry
    if (doPhotom) {
        // This file, PSPHOT.INPUT, is just used as a carrier; output files (eg, PSPHOT.RESID) are defined by
        // psphotDefineFiles
        pmFPAfile *psphotInput = pmFPAfileDefineFromFPA(config, output->fpa, 1, 1, "PSPHOT.INPUT");
        if (!psphotInput) {
            psError(PS_ERR_IO, false, _("Unable to generate output file from PSPHOT.INPUT"));
            return 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;
        }
    }

    return true;
}
