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

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

#include "pmConfig.h"
#include "pmFPA.h"
#include "pmFPAConstruct.h"
#include "pmFPAview.h"
#include "pmFPAEnsemble.h"


static void fpaComponentFree(pmFPAComponent *component)
{
    psFree(component->fpa);
    psFree(component->view);
    return;
}

pmFPAComponent *pmFPAComponentAlloc(pmFPA *fpa, pmFPAview *view)
{
    pmFPAComponent *component = psAlloc(sizeof(pmFPAComponent));
    psMemSetDeallocator(component, (psFreeFunc)fpaComponentFree);
    component->fpa = psMemIncrRefCounter(fpa);
    component->view = psMemIncrRefCounter(view);
    return component;
}

bool pmFPAComponentSetExist(pmFPAComponent *component)
{
    pmFPA *fpa = component->fpa;        // The FPA of interest
    pmFPAview *view = component->view;  // The view of interest

    if (view->chip < 0) {
        // This iterates on all chips
        return pmFPASetFileStatus(fpa, true);
    }
    psArray *chips = fpa->chips;        // Array of chips
    if (view->chip >= chips->n) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "FPA view refers to value out of range (%d)\n", view->chip);
        return false;
    }
    pmChip *chip = chips->data[view->chip]; // Chip of interest
    if (view->cell < 0) {
        // This iterates on all cells
        return pmChipSetFileStatus(chip, true);
    }
    psArray *cells = chip->cells;       // Array of cells
    if (view->cell >= cells->n) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "FPA view refers to value out of range (%d)\n", view->cell);
        return false;
    }
    pmCell *cell = cells->data[view->cell]; // Cell of interest
    return pmCellSetFileStatus(cell, true);
}

bool pmFPAComponentCheckExist(pmFPAComponent *component)
{
    pmFPA *fpa = component->fpa;        // The FPA of interest
    pmFPAview *view = component->view;  // The view of interest

    if (view->chip < 0) {
        // This iterates on all chips
        return pmFPACheckFileStatus(fpa);
    }
    psArray *chips = fpa->chips;        // Array of chips
    if (view->chip >= chips->n) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "FPA view refers to value out of range (%d)\n", view->chip);
        return false;
    }
    pmChip *chip = chips->data[view->chip]; // Chip of interest
    if (view->cell < 0) {
        // This iterates on all cells
        return pmChipCheckFileStatus(chip);
    }
    psArray *cells = chip->cells;       // Array of cells
    if (view->cell >= cells->n) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "FPA view refers to value out of range (%d)\n", view->cell);
        return false;
    }
    pmCell *cell = cells->data[view->cell]; // Cell of interest
    return pmCellCheckFileStatus(cell);
}


// Move a cell from one chip to another
static bool cellMove(pmChip *target,    // Target chip
                     pmChip *source,    // Source chip
                     int cellNum        // Number of cell
                    )
{
    assert(cellNum > 0);
    assert(cellNum < source->cells->n);

    pmCell *sourceCell = source->cells->data[cellNum]; // The source cell
    pmCell *targetCell = target->cells->data[cellNum]; // The target cell
    // Switch ownership of the cell
    psFree(targetCell);
    target->cells->data[cellNum] = sourceCell;
    source->cells->data[cellNum] = NULL;
    sourceCell->parent = target;

    return true;
}

// Move a chip from one FPA to another
static bool chipMove(pmFPA *target,     // Target FPA
                     pmFPA *source,     // Source FPA
                     int chipNum        // Number of chip
                    )
{
    assert(chipNum > 0);
    assert(chipNum < source->chips->n);

    pmChip *sourceChip = source->chips->data[chipNum]; // The source chip
    pmChip *targetChip = target->chips->data[chipNum]; // The target chip
    // Switch ownership of the chip
    psFree(targetChip);
    target->chips->data[chipNum] = sourceChip;
    source->chips->data[chipNum] = NULL;
    sourceChip->parent = target;

    return true;
}



bool pmFPAComponentCopy(pmFPAComponent *target, pmFPA *source)
{
    pmFPA *targetFPA = target->fpa;     // The FPA of interest
    pmFPAview *view = target->view;     // The view of interest

    if (view->chip < 0) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "This FPA already exists.\n");
        return false;
    }

    psArray *targetChips = targetFPA->chips; // Array of chips
    if (view->chip >= targetChips->n) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "FPA view refers to value out of range (%d)\n", view->chip);
        return false;
    }
    if (view->cell < 0) {
        // Move the entire chip over
        return chipMove(targetFPA, source, view->chip);
    }
    pmChip *targetChip = targetChips->data[view->chip]; // Target chip of interest
    pmChip *sourceChip = source->chips->data[view->chip]; // Source chip of interest
    psArray *targetCells = targetChip->cells; // Array of cells
    if (view->cell >= targetCells->n) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "FPA view refers to value out of range (%d)\n", view->cell);
        return false;
    }
    // Move the cell over
    return cellMove(targetChip, sourceChip, view->cell);
}




static void fpaEnsembleFree(pmFPAEnsemble *ensemble)
{
    psFree(ensemble->fpa);
    psFree(ensemble->fits);
    psFree(ensemble->fpaToFits);
    psFree(ensemble->fitsToFpa);
    return;
}

pmFPAEnsemble *pmFPAEnsembleAlloc(psArray *filenames, // Array of file names
                                  pmConfig *config // Configuration
                                 )
{
    int numFiles = filenames->n;        // Number of files
    PS_ASSERT_INT_POSITIVE(numFiles, NULL);

    // The objects of interest
    psArray *fits = psArrayAlloc(numFiles); // Array of file pointers
    fits->n = numFiles;
    psMetadata *fpas = psMetadataAlloc(); // The focal plane arrays, indexed by FPA.NAME

    // Translations
    psArray *fitsToFPA = psArrayAlloc(numFiles); // Array with FPAs for each FITS file
    fitsToFPA->n = numFiles;
    psMetadata *fpaToFits = psMetadataAlloc(); // The FITS file for each FPA, indexed by FPA.NAME

    for (int i = 0; i < numFiles; i++) {
        // Open the file, read the header, get the camera format, generate an FPA structure
        const char *filename = filenames->data[i]; // File name
        psFits *fp = psFitsOpen(filename, "r"); // File pointer
        if (!fp) {
            psLogMsg(__func__, PS_LOG_WARN, "Unable to open %s --- ignored.\n", filename);
            continue;
        }
        psMetadata *header = psFitsReadHeader(NULL, fp); // Primary header from the FITS file
        if (!header) {
            psLogMsg(__func__, PS_LOG_WARN, "Unable to read header for %s --- ignored.\n", filename);
            psFitsClose(fp);
            continue;
        }
        psMetadata *format = pmConfigCameraFormatFromHeader(config, header);
        if (!format) {
            psLogMsg(__func__, PS_LOG_WARN, "Unable to determine camera format for %s --- ignored.\n",
                     filename);
            psFree(header);
            psFitsClose(fp);
            continue;
        }
        // Construct camera in preparation for reading
        pmFPA *fpa = pmFPAConstruct(config->camera); // An FPA to contain only this file
        if (!fpa) {
            psLogMsg(__func__, PS_LOG_WARN, "Unable to construct FPA for %s --- ignored.\n", filename);
            psFree(format);
            psFree(header);
            psFitsClose(fp);
            continue;
        }
        pmFPAview *view = pmFPAAddSourceFromHeader(fpa, header, format); // View corresponding to the PHU
        if (!view) {
            psLogMsg(__func__, PS_LOG_WARN, "Unable to add file to FPA for %s --- ignored.\n", filename);
            psFree(fpa);
            psFree(format);
            psFree(header);
            psFitsClose(fp);
            continue;
        }

        // Put this into any existing FPA
        bool mdok;                      // Status of MD lookup
        const char *fpaName = psMetadataLookupStr(&mdok, fpa->concepts, "FPA.NAME"); // Name of the FPA
        if (!mdok || !fpaName || strlen(fpaName) == 0) {
            psLogMsg(__func__, PS_LOG_WARN, "Unable to find FPA.NAME for %s --- ignored.\n", filename);
            psFree(view);
            psFree(fpa);
            psFree(format);
            psFree(header);
            psFitsClose(fp);
            continue;
        }

        psTrace("psModules.camera", 3, "%s --> FPA: %s, chip: %d, cell: %d\n", filename, fpaName,
                view->chip, view->cell);

        pmFPAComponent *component;      // FPA component for the new file
        psMetadataItem *check = psMetadataLookup(fpas, fpaName); // Check for an existing FPA with same name
        if (!check) {
            // Add in the new FPA
            psTrace("psModules.camera", 5, "New FPA.\n");
            component = pmFPAComponentAlloc(fpa, view);
            psMetadataAddPtr(fpas, PS_LIST_TAIL, fpaName, PS_DATA_UNKNOWN, NULL, component->fpa);
        } else {
            // Need to put the appropriate element of the new FPA in the old one.
            psTrace("psModules.camera", 5, "Adding to extant FPA.\n");
            pmFPA *oldFPA = check->data.V; // The existing FPA
            component = pmFPAComponentAlloc(oldFPA, view);
            // Check that it doesn't already exist
            if (pmFPAComponentCheckExist(component)) {
                psLogMsg(__func__, PS_LOG_WARN, "The component for FPA %s in file %s already exists --- "
                         "ignored.\n", fpaName, filename);
                psFree(component);
                psFree(view);
                psFree(fpa);
                psFree(format);
                psFree(header);
                psFitsClose(fp);
                continue;
            }

            if (!pmFPAComponentCopy(component, fpa)) {
                psLogMsg(__func__, PS_LOG_WARN, "Unable to copy FPA component for %s --- ignored.\n",
                         filename);
                psFree(component);
                psFree(view);
                psFree(fpa);
                psFree(format);
                psFree(header);
                psFitsClose(fp);
                continue;
            }
        }
        pmFPAComponentSetExist(component);

        fits->data[i] = fp;
        // Fill in the translations
        fitsToFPA->data[i] = component;
        psMetadataAddS32(fpaToFits, PS_LIST_TAIL, fpaName, PS_META_DUPLICATE_OK, NULL, i);
    }

    pmFPAEnsemble *ensemble = psAlloc(sizeof(pmFPAEnsemble)); // The ensemble, to be returned
    psMemSetDeallocator(ensemble, (psFreeFunc)fpaEnsembleFree);

    ensemble->fits = fits;
    ensemble->fpa = fpas;
    ensemble->fitsToFpa = fitsToFPA;
    ensemble->fpaToFits = fpaToFits;

    return ensemble;
}
