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

#include <ppStats.h>
#include "ppImage.h"
#include "ppImageDetrendFringe.h"
#include "ppImageVersion.h"

bool ppImageLoop (pmConfig *config, ppImageOptions *options) {

    bool status;
    pmChip *chip;
    pmCell *cell;
    pmReadout *readout;

    bool mdok;                      // Status of MD lookup
    psMetadata *recipe = psMetadataLookupMetadata(&mdok, config->recipes, RECIPE_NAME);
    const char *statsName = psMetadataLookupStr(&mdok, recipe, "STATS"); // Filename for statistics
    psMetadata *stats = NULL;           // Container for statistics
    FILE *statsFile = NULL;             // File stream for statistics
    if (mdok && statsName && strlen(statsName) > 0) {
        statsFile = fopen(statsName, "w");
        if (!statsFile) {
            psError(PS_ERR_IO, true, "Unable to open statistics file %s for writing.\n", statsName);
        } else {
            stats = psMetadataAlloc();
        }
    }

    pmFPAfile *input = psMetadataLookupPtr(&status, config->files, "PPIMAGE.INPUT");
    if (!status) {
        psErrorStackPrint(stderr, "Can't find input data!\n");
        exit(EXIT_FAILURE);
    }

    pmFPAview *view = pmFPAviewAlloc(0);// View for level of interest
    pmHDU *lastHDU = NULL;              // Last HDU that was updated

    // files associated with the science image
    if (!pmFPAfileIOChecks(config, view, PM_FPA_BEFORE)) {
        return false;
    }

    while ((chip = pmFPAviewNextChip(view, input->fpa, 1)) != NULL) {
        psLogMsg ("ppImageLoop", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
        if (!chip->process || !chip->file_exists) {
            continue;
        }

        if (!pmFPAfileIOChecks(config, view, PM_FPA_BEFORE)) {
            return false;
        }

        while ((cell = pmFPAviewNextCell(view, input->fpa, 1)) != NULL) {
            psLogMsg ("ppImageLoop", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
            if (!cell->process || !cell->file_exists) {
                continue;
            }
            if (!pmFPAfileIOChecks(config, view, PM_FPA_BEFORE)) {
                return false;
            }

            // Put version information into the header
            pmHDU *hdu = pmHDUFromCell(cell);
            if (hdu && hdu != lastHDU) {
                ppImageVersionMetadata(hdu->header);
                lastHDU = hdu;
            }


            // process each of the readouts
            while ((readout = pmFPAviewNextReadout (view, input->fpa, 1)) != NULL) {
                if (!pmFPAfileIOChecks(config, view, PM_FPA_BEFORE)) {
                    return false;
                }
                if (!readout->data_exists) {
                    continue;
                }

                // perform the detrend analysis
                if (!ppImageDetrendReadout(config, options, view)) {
                    psError(PS_ERR_UNKNOWN, false, "Unable to detrend readout.\n");
                    return false;
                }

            }
        }

        // Solve the fringe system
        if (options->doFringe) {
            pmChip *fringe = pmFPAfileThisChip(config->files, view, "PPIMAGE.FRINGE");
            if (!ppImageDetrendFringeSolve(chip, fringe, options)) {
                return false;
            }
        }

        // Go back over the cells to apply the fringe and do statistics
        view->cell = view->readout = -1;
        while ((cell = pmFPAviewNextCell(view, input->fpa, 1)) != NULL) {
            if (!cell->process || !cell->file_exists) {
                continue;
            }

            // Apply the fringe correction
            if (options->doFringe) {
                psTrace("ppImage", 3, "Applying fringe correction...\n");
                pmCell *fringeCell = pmFPAfileThisCell(config->files, view, "PPIMAGE.FRINGE");
                if (!ppImageDetrendFringeGenerate(cell, fringeCell)) {
                    return false;
                }
            }

            // Perform statistics on the detrended cell
            if (stats) {
                bool mdok;              // Status of MD lookup
                pmFPAfile *output = psMetadataLookupPtr(&mdok, config->files, "PPIMAGE.OUTPUT");
                if (!mdok || !output) {
                    psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find file PPIMAGE.OUTPUT.\n");
                    return false;
                }
                ppStats(stats, output->fpa, view, config);
            }

            // Add MD5 information for cell
            pmHDU *hdu = pmHDUFromCell(cell); // HDU that owns the cell
            while ((readout = pmFPAviewNextReadout(view, input->fpa, 1)) != NULL) {
                const char *chipName = psMetadataLookupStr(NULL, chip->concepts, "CHIP.NAME");
                const char *cellName = psMetadataLookupStr(NULL, cell->concepts, "CELL.NAME");

                psString headerName = NULL; // Header name for MD5
                psStringAppend(&headerName, "MD5_%s_%s_%d", chipName, cellName, view->readout);

                psVector *md5 = psImageMD5(readout->image); // md5 hash
                psString md5string = psMD5toString(md5); // String
                psFree(md5);
                psMetadataAddStr(hdu->header, PS_LIST_TAIL, headerName, PS_META_REPLACE,
                                 "Image MD5", md5string);
                psFree(md5string);
                psFree(headerName);
            }

#if 0
            if (!pmFPAfileIOChecks(config, view, PM_FPA_AFTER)) {
                return false;
            }
#endif

        }

        if (!ppImageMosaicChip(config, view, "PPIMAGE.OUTPUT.CHIP", "PPIMAGE.OUTPUT")) {
            psError(PS_ERR_UNKNOWN, false, "Unable to mosaic chip.\n");
            return false;
        }
        if (!ppImageRebinChip(config, view, "PPIMAGE.BIN1", "PPIMAGE.OUTPUT.CHIP")) {
            psError(PS_ERR_UNKNOWN, false, "Unable to bin chip.\n");
            return false;
        }
        if (!ppImageRebinChip(config, view, "PPIMAGE.BIN2", "PPIMAGE.OUTPUT.CHIP")) {
            psError(PS_ERR_UNKNOWN, false, "Unable to bin chip.\n");
            return false;
        }

        // we perform photometry on the readouts of this chip in the output
        if (options->doPhotom) {
            ppImagePhotom(config, view);
        }

        // Close cells
        view->cell = -1;
        while ((cell = pmFPAviewNextCell(view, input->fpa, 1)) != NULL) {
            if (!cell->process || !cell->file_exists) {
                continue;
            }

            if (!pmFPAfileIOChecks(config, view, PM_FPA_AFTER)) {
                return false;
            }
        }

        // Close chip
        if (!pmFPAfileIOChecks(config, view, PM_FPA_AFTER)) {
            return false;
        }
    }

    // Write out summary statistics
    if (stats) {
        const char *statsMDC = psMetadataConfigFormat(stats);
        if (!statsMDC || strlen(statsMDC) == 0) {
            psError(PS_ERR_IO, false, "Unable to get statistics MDC file.\n");
        } else {
            fprintf(statsFile, "%s", statsMDC);
        }
        psFree((void *)statsMDC);
        fclose(statsFile);

        psFree(stats);
    }

    // XXX an optional full-scale FPA mosaic could happen here
    // it would require us to NOT free PPIMAGE.OUTPUT.CHIP until here
    // ppImageMosaicFPA (config, "PPIMAGE.OUTPUT.FPA", "PPIMAGE.OUTPUT.CHIP");
    ppImageMosaicFPA(config, "PPIMAGE.OUTPUT.FPA1", "PPIMAGE.BIN1");
    ppImageMosaicFPA(config, "PPIMAGE.OUTPUT.FPA2", "PPIMAGE.BIN2");

    // we perform astrometry on all chips after sources have been detected
    if (options->doAstromChip || options->doAstromMosaic) {
        ppImageAstrom(config);
    }
    // XXX should we keep the addstar command as an option, or just run it externally?
    if (options->doAddstar) {
        ppImageAddstar(config);
    }

    if (!pmFPAfileIOChecks(config, view, PM_FPA_AFTER)) {
        return false;
    }

    psFree(view);

    return true;
}
