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

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

#include "pmHDU.h"
#include "pmFPA.h"
#include "pmFPAUtils.h"
#include "pmHDUUtils.h"
#include "pmFPACopy.h"

//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// File-static functions and macros
//////////////////////////////////////////////////////////////////////////////////////////////////////////////

// Copy the value for a concept
#define COPY_CONCEPT(TARGET, SOURCE, NAME, TYPE) { \
    psMetadataItem *targetItem = psMetadataLookup(TARGET, NAME); \
    psMetadataItem *sourceItem = psMetadataLookup(SOURCE, NAME); \
    targetItem->data.TYPE = sourceItem->data.TYPE; }

// Find the blank (image-less) PHU, given a cell.
static pmHDU *findBlankPHU(const pmCell *cell // The cell for which to find the PHU
                          )
{
    assert(cell);

    if (cell->hdu && cell->hdu->blankPHU) {
        return cell->hdu;
    }
    pmChip *chip = cell->parent;        // The parent chip
    if (chip->hdu && chip->hdu->blankPHU) {
        return chip->hdu;
    }
    pmFPA *fpa = chip->parent;  // The parent FPA
    if (fpa->hdu && fpa->hdu->blankPHU) {
        return fpa->hdu;
    }

    return NULL;
}

// copy one of the psImage components of the readout
static void readoutCopyComponent (psImage **target, psImage *source, psImageBinning *binning, bool xFlip, bool yFlip, bool pixels) {

    if (!source) return;

    if (*target) {
        psFree(*target);
    }
    if (pixels) {
        *target = psImageFlip(NULL, source, xFlip, yFlip);
        return;
    }

    // I have the fine image size, I know the binning factor, determine the ruff image size
    binning->nXfine = source->numCols;
    binning->nYfine = source->numRows;
    psImageBinningSetRuffSize(binning, PS_IMAGE_BINNING_CENTER);
    *target = psImageAlloc(binning->nXruff, binning->nYruff, source->type.type);
    return;
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// File-static engine functions --- these do all the work.  Actually, cellCopy does all the work; the others
// merely iterate on the higher-level components.
//////////////////////////////////////////////////////////////////////////////////////////////////////////////

// Common engine for pmCellCopy and pmCellCopyStructure
// Does the actual splitting/splicing that's required to copy an FPA to a different representation.
static bool cellCopy(pmCell *target,     // The target cell
                     const pmCell *source, // The source cell, to be copied
                     bool pixels,        // Copy the pixels?
                     int xBin, int yBin  // (Relative) binning factors in x and y
                    )
{
    assert(target);
    assert(source);
    assert(xBin > 0 && yBin > 0);

    if (!source->data_exists) {
        // Copied everything that exists
        return true;
    }

    // XXX this is a programming / config error
    if (pixels && (xBin != 1 || yBin != 1)) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unable to copy pixels if binning is set\n");
        return false;
    }

    // the binning structure carries the information no how to rebin the images if needed
    psImageBinning *binning = psImageBinningAlloc();
    binning->nXbin = xBin;
    binning->nYbin = yBin;

    psArray *sourceReadouts = source->readouts; // The source readouts
    int numReadouts = sourceReadouts->n; // Number of readouts copied

    // Need to check/change CELL.XPARITY and CELL.YPARITY
    bool mdokS = true;                   // Status of MD lookup
    bool mdokT = true;                   // Status of MD lookup
    bool xFlip = false;                 // Switch parity in x?
    bool yFlip = false;                 // Switch parity in y?

    // enforce the following conditions:
    // CELL.XPARITY is required
    // CELL.XPARITY must be +/- 1
    int xParitySource = psMetadataLookupS32(&mdokS, source->concepts, "CELL.XPARITY"); // Source parity
    int xParityTarget = psMetadataLookupS32(&mdokT, target->concepts, "CELL.XPARITY"); // Target x parity
    assert(mdokS && mdokT);
    if (abs(xParitySource) != 1 || abs(xParityTarget) != 1) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
                "CELL.XPARITY is not set for both source (%d) and target (%d)",
                xParitySource, xParityTarget);
        psFree(binning);
        return false;
    }
    if (xParityTarget != xParitySource) {
        xFlip = true;
    } else {
        // Use the source parity
        COPY_CONCEPT(target->concepts, source->concepts, "CELL.XPARITY", S32);
    }

    int yParityTarget = psMetadataLookupS32(&mdokT, target->concepts, "CELL.YPARITY"); // Target y parity
    int yParitySource = psMetadataLookupS32(&mdokS, source->concepts, "CELL.YPARITY"); // Source parity
    assert(mdokS && mdokT);
    if (abs(yParitySource) != 1 || abs(yParityTarget) != 1) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
                "CELL.YPARITY is not set for both source (%d) and target (%d)",
                yParitySource, yParityTarget);
        psFree(binning);
        return false;
    }
    if (yParityTarget != yParitySource) {
        yFlip = true;
    } else {
        // Use the source parity
        COPY_CONCEPT(target->concepts, source->concepts, "CELL.YPARITY", S32);
    }
    psTrace("psModules.camera", 3, "xFlip: %d; yFlip: %d\n", xFlip, yFlip);

    // Perform deep copy of the images.  I would prefer *not* to do a deep copy, in the interests of speed (we
    // still need to do another deep copy into the HDU for when we write out), but this is the only way I can
    // think of to provide security against copying a cell and then unknowingly changing the source when
    // manipulating the target.
    for (int i = 0; i < numReadouts; i++) {
        pmReadout *sourceReadout = sourceReadouts->data[i]; // The source readout
        pmReadout *targetReadout = pmReadoutAlloc(target); // The target readout; this adds it to the cell

        // Copy attributes
        // XXX is this correct under binning?
        targetReadout->col0 = sourceReadout->col0;
        targetReadout->row0 = sourceReadout->row0;
        targetReadout->process = sourceReadout->process;
        targetReadout->file_exists = sourceReadout->file_exists;
        targetReadout->data_exists = sourceReadout->data_exists;

        // Copy all three image components (image, mask, weight)
        readoutCopyComponent (&targetReadout->image,  sourceReadout->image,  binning, xFlip, yFlip, pixels);
        readoutCopyComponent (&targetReadout->mask,   sourceReadout->mask,   binning, xFlip, yFlip, pixels);
        readoutCopyComponent (&targetReadout->weight, sourceReadout->weight, binning, xFlip, yFlip, pixels);

        // Copy bias
        while (targetReadout->bias->n > 0) {
            psListRemove(targetReadout->bias, PS_LIST_HEAD);
        }

        // Iterate over the biases
        psListIterator *biasIter = psListIteratorAlloc(sourceReadout->bias, PS_LIST_HEAD, false);
        psImage *bias = NULL;           // Bias image from iteration
        while ((bias = psListGetAndIncrement(biasIter))) {
            psImage *biasCopy = NULL;          // Copy of the bias
            readoutCopyComponent (&biasCopy, bias, binning, xFlip, yFlip, pixels);
            psListAdd(targetReadout->bias, PS_LIST_TAIL, biasCopy);
            psFree(biasCopy);           // Drop reference
        }
        psFree(biasIter);

        targetReadout->data_exists = true;
        psFree(targetReadout);          // Drop reference
    }

    // Copy the remaining "concepts" over.  Don't copy the TRIMSEC or BIASSEC, since these will be created by
    // pmHDUGenerate if they don't already exist in the target.  Don't copy the XPARITY or YPARITY, since
    // we've used those to do the flips.  Don't copy the X0 and Y0 because they are updated below (and are
    // dependent upon the flips we've done above).
    psMetadataIterator *conceptsIter = psMetadataIteratorAlloc(source->concepts, PS_LIST_HEAD, NULL);
    psMetadataItem *conceptItem = NULL; // Item from iteration
    while ((conceptItem = psMetadataGetAndIncrement(conceptsIter))) {
        psString name = conceptItem->name; // Name of concept
        if (!strcmp(name, "CELL.TRIMSEC")) continue;
        if (!strcmp(name, "CELL.BIASSEC")) continue;
        if (!strcmp(name, "CELL.XPARITY")) continue;
        if (!strcmp(name, "CELL.YPARITY")) continue;
        if (!strcmp(name, "CELL.X0")) continue;
        if (!strcmp(name, "CELL.Y0")) continue;

        psMetadataItem *copy = psMetadataItemCopy(conceptItem); // Copy of the concept
        psMetadataAddItem(target->concepts, copy, PS_LIST_TAIL, PS_META_REPLACE);
        psFree(copy);               // Drop reference
    }
    psFree(conceptsIter);

    // Need to update CELL.TRIMSEC and CELL.BIASSEC if we changed the binning and they exist already.
    // XXX this code seems to be very similar to pmConceptsUpdate
    if ((binning->nXbin != 1) || (binning->nYbin != 1)) {
        bool mdok = false;
        psRegion *trimsec = psMetadataLookupPtr(&mdok, target->concepts, "CELL.TRIMSEC"); // The trim section
        if (mdok && trimsec && !psRegionIsNaN(*trimsec)) {
            *trimsec = psImageBinningSetRuffRegion(binning, *trimsec);
            // force integer pixels : truncate x0, roundup x1:
            trimsec->x0 = (int)trimsec->x0;
            if (trimsec->x1 > (int)trimsec->x1) {
                trimsec->x1 = (int)trimsec->x1 + 1;
            } else {
                trimsec->x1 = (int)trimsec->x1;
            }
            trimsec->y0 = (int)trimsec->y0;
            if (trimsec->y1 > (int)trimsec->y1) {
                trimsec->y1 = (int)trimsec->y1 + 1;
            } else {
                trimsec->y1 = (int)trimsec->y1;
            }
        }
        psList *biassecs = psMetadataLookupPtr(&mdok, target->concepts, "CELL.BIASSEC"); // The bias sections
        if (mdok && biassecs && biassecs->n > 0) {
            psListIterator *biassecsIter = psListIteratorAlloc(biassecs, PS_LIST_HEAD, true); // Iterator
            psRegion *biassec = NULL;   // Bias section, from iteration
            while ((biassec = psListGetAndIncrement(biassecsIter))) {
                if (!psRegionIsNaN(*biassec)) {
                    *biassec = psImageBinningSetRuffRegion(binning, *biassec);
                    // force integer pixels : truncate x0, roundup x1:
                    biassec->x0 = (int)biassec->x0;
                    if (biassec->x1 > (int)biassec->x1) {
                        biassec->x1 = (int)biassec->x1 + 1;
                    } else {
                        biassec->x1 = (int)biassec->x1;
                    }
                    biassec->y0 = (int)biassec->y0;
                    if (biassec->y1 > (int)biassec->y1) {
                        biassec->y1 = (int)biassec->y1 + 1;
                    } else {
                        biassec->y1 = (int)biassec->y1;
                    }
                }
            }
            psFree(biassecsIter);
        }
    }

    // Need to update CELL.X0 and CELL.Y0 if we flipped
    // XXX this section should probably use a common function consistent with psImageBinning
    if (xFlip) {
        int xZero = psMetadataLookupS32(NULL, source->concepts, "CELL.X0"); // CELL.X0 from source
        int xParity = psMetadataLookupS32(NULL, source->concepts, "CELL.XPARITY"); // Parity in x
        int sourceBin = psMetadataLookupS32(NULL, source->concepts, "CELL.XBIN"); // CELL.XBIN from source
        int xSize = psMetadataLookupS32(NULL, source->concepts, "CELL.XSIZE"); // CELL.XSIZE of source

        if (sourceBin == 0) {
            // Don't know the binning; assume it is unity
            sourceBin = binning->nXbin;
        }

        // XXX make sure this is consistent with the psImageBinning
        psTrace("psModules.camera", 3, "CELL.X0: Before: %d After: %d\n", xZero, xZero + (xSize - 1) * xParity * sourceBin);
        psTrace("psModules.camera", 9, "(xParity: %d xBin: %d numCols: %d)\n", xParity, sourceBin, xSize);

        if (xParity == 0 || xSize == 0) {
            psWarning("New CELL.X0 may be incorrect due to missing concepts (CELL.XPARITY, CELL.XSIZE)");
        }

        xZero += (xSize - 1) * xParity * sourceBin; // Change the parity on the X0 position
        psMetadataItem *newItem = psMetadataLookup(target->concepts, "CELL.X0"); // CELL.X0 from target
        newItem->data.S32 = xZero;
    }
    if (yFlip) {
        int yZero = psMetadataLookupS32(NULL, source->concepts, "CELL.Y0"); // CELL.Y0 from source
        int yParity = psMetadataLookupS32(NULL, source->concepts, "CELL.YPARITY"); // Parity in y
        int sourceBin = psMetadataLookupS32(NULL, source->concepts, "CELL.YBIN"); // Binning in y
        int ySize = psMetadataLookupS32(NULL, source->concepts, "CELL.YSIZE"); // CELL.YSIZE of source

        if (sourceBin == 0) {
            // Don't know the binning; assume it is unity
            sourceBin = binning->nYbin;
        }

        psTrace("psModules.camera", 3, "CELL.Y0: Before: %d After: %d\n", yZero, yZero + (ySize - 1) * yParity * sourceBin);
        psTrace("psModules.camera", 9, "(yParity: %d yBin: %d numRows: %d)\n", yParity, sourceBin, ySize);

        if (yParity == 0 || ySize == 0) {
            psWarning("New CELL.Y0 may be incorrect due to missing concepts "
                      "(CELL.Y0, CELL.YPARITY, CELL.YBIN, CELL.YSIZE)");
        }

        yZero += (ySize - 1) * yParity * sourceBin; // Change the parity on the Y0 position
        psMetadataItem *newItem = psMetadataLookup(target->concepts, "CELL.Y0"); // CELL.Y0 from target
        newItem->data.S32 = yZero;
    }

    // Update the binning concepts
    // XXX this should probably be done with a common function using psImageBinning
    psMetadataItem *binItem = psMetadataLookup(target->concepts, "CELL.XBIN");
    binItem->data.S32 *= xBin;
    binItem = psMetadataLookup(target->concepts, "CELL.YBIN");
    binItem->data.S32 *= yBin;

    // Copy any headers
    pmHDU *targetHDU = pmHDUFromCell(target); // The target HDU
    if (targetHDU) {
        pmHDU *sourceHDU = pmHDUFromCell(source); // The source HDU
        if (!sourceHDU) {
            psWarning("Unable to copy header: no source header.");
        } else if (sourceHDU->header) {
            targetHDU->header = psMetadataCopy(targetHDU->header, sourceHDU->header);
        }
    }

    // Copy the PHU over as well, if required
    pmHDU *targetPHU = findBlankPHU(target); // The target PHU
    if (targetPHU && targetPHU != targetHDU) {
        // pmHDU *sourcePHU = pmHDUGetHighest(source->parent->parent, source->parent, source); // A source HDU
        pmHDU *sourcePHU = findBlankPHU(source); // The target PHU
        if (sourcePHU && sourcePHU->header) {
            targetPHU->header = psMetadataCopy(targetPHU->header, sourcePHU->header);
        }
    }

    psFree (binning);
    target->data_exists = true;
    target->parent->data_exists = true;
    return true;
}

// Common engine for pmChipCopy and pmChipCopyStructure
// Iterate on the components
static bool chipCopy(pmChip *target,          // The target chip
                     const pmChip *source, // The source chip, to be copied
                     bool pixels,             // Copy the pixels?
                     int xBin, int yBin       // (Relative) binning factors in x and y
                    )
{
    assert(target);
    assert(source);
    assert(xBin > 0);
    assert(yBin > 0);

    if (!source->data_exists) {
        // Copied everything that exists
        return true;
    }

    psArray *targetCells = target->cells; // The target cells
    psArray *sourceCells = source->cells; // The source cells
    if (targetCells->n != sourceCells->n) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
                "Number of source cells (%ld) differs from the number of target cells (%ld)\n",
                sourceCells->n, targetCells->n);
        return false;
    }

    bool status = true;                 // Status of copy
    for (int i = 0; i < targetCells->n; i++) {
        pmCell *targetCell = targetCells->data[i]; // The target cell
        const char *cellName = psMetadataLookupStr(NULL, targetCell->concepts, "CELL.NAME"); // Name of cell
        int cellNum = pmChipFindCell(source, cellName); // Number of cell with that name
        if (cellNum >= 0) {
            pmCell *sourceCell = sourceCells->data[cellNum]; // The source cell
            status &= cellCopy(targetCell, sourceCell, pixels, xBin, yBin);
        }
    }

    // Update the concepts
    psMetadataItem *chipName = psMemIncrRefCounter(psMetadataLookup(target->concepts, "CHIP.NAME"));
    psMetadataCopy(target->concepts, source->concepts);
    psMetadataAddItem(target->concepts, chipName, PS_LIST_TAIL, PS_META_REPLACE);
    psFree(chipName);
    psMetadataCopy(target->parent->concepts, source->parent->concepts);

    target->data_exists = true;
    return status;
}

// create a new pmChip with the data derived from the supplied chip
pmChip *pmChipDuplicate(pmFPA *fpa, const pmChip *sourceChip)
{
    assert(sourceChip);

    bool status;
    char *chipName = psMetadataLookupStr(&status, sourceChip->concepts, "CHIP.NAME");
    pmChip *targetChip = pmChipAlloc (NULL, chipName);
    targetChip->parent = fpa;

    psArray *sourceCells = sourceChip->cells; // The source cells

    for (int i = 0; i < sourceCells->n; i++) {
        pmCell *sourceCell = sourceCells->data[i]; // The sources cell
        const char *cellName = psMetadataLookupStr(NULL, sourceCell->concepts, "CELL.NAME"); // Name of cell
        // XXX are there other concepts I need to copy first?
        pmCell *targetCell = pmCellAlloc (targetChip, cellName);
        int xParityTarget = psMetadataLookupS32(&status, sourceCell->concepts, "CELL.XPARITY"); // Target x parity
        psMetadataAddS32 (targetCell->concepts, PS_LIST_TAIL, "CELL.XPARITY", PS_META_REPLACE, "", xParityTarget);
        int yParityTarget = psMetadataLookupS32(&status, sourceCell->concepts, "CELL.YPARITY"); // Target y parity
        psMetadataAddS32 (targetCell->concepts, PS_LIST_TAIL, "CELL.YPARITY", PS_META_REPLACE, "", yParityTarget);
        if (!cellCopy(targetCell, sourceCell, true, 1, 1)) {
            psError(PS_ERR_BAD_PARAMETER_VALUE, false, "failed to duplicate chip\n");
            return NULL;
        }
        // update the attributes
        targetCell->file_exists = sourceCell->file_exists;
        targetCell->data_exists = sourceCell->data_exists;
        targetCell->process     = sourceCell->process;
    }

    // Update the concepts
    psMetadataCopy(targetChip->concepts, sourceChip->concepts);

    // update the attributes
    targetChip->file_exists = sourceChip->file_exists;
    targetChip->data_exists = sourceChip->data_exists;
    targetChip->process     = sourceChip->process;

    return targetChip;
}

// Common engine for pmFPACopy and pmFPACopyStructure.
// Iterate on the components
static bool fpaCopy(pmFPA *target,      // The target FPA
                    const pmFPA *source, // The source FPA, to be copied
                    bool pixels,        // Copy the pixels?
                    int xBin, int yBin  // (Relative) binning factors in x and y
                   )
{
    assert(target);
    assert(source);
    assert(xBin > 0);
    assert(yBin > 0);

    psArray *targetChips = target->chips; // The target chips
    psArray *sourceChips = source->chips; // The source chips
    if (targetChips->n != sourceChips->n) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
                "Number of source chips (%ld) differs from the number of target chips (%ld)\n",
                sourceChips->n, targetChips->n);
        return false;
    }

    bool status = true;                 // Status of copy
    for (int i = 0; i < targetChips->n; i++) {
        pmChip *targetChip = targetChips->data[i]; // The target chip
        const char *chipName = psMetadataLookupStr(NULL, targetChip->concepts, "CHIP.NAME"); // Name of chip
        int chipNum = pmFPAFindChip(source, chipName); // Number of chip with that name
        if (chipNum >= 0) {
            pmChip *sourceChip = sourceChips->data[chipNum]; // The source chip
            status &= chipCopy(targetChip, sourceChip, pixels, xBin, yBin);
        }
    }

    // Update the concepts
    psMetadataCopy(target->concepts, source->concepts);

    return status;
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Public functions
//////////////////////////////////////////////////////////////////////////////////////////////////////////////

bool pmFPACopy(pmFPA *target, const pmFPA *source)
{
    PS_ASSERT_PTR_NON_NULL(target, false);
    PS_ASSERT_PTR_NON_NULL(source, false);
    if (target == source) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Can't copy FPA onto itself.");
        return false;
    }
    return fpaCopy(target, source, true, 1, 1);
}

bool pmChipCopy(pmChip *target, const pmChip *source)
{
    PS_ASSERT_PTR_NON_NULL(target, false);
    PS_ASSERT_PTR_NON_NULL(source, false);
    if (target == source) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Can't copy chip onto itself.");
        return false;
    }
    return chipCopy(target, source, true, 1, 1);
}

bool pmCellCopy(pmCell *target, const pmCell *source)
{
    PS_ASSERT_PTR_NON_NULL(target, false);
    PS_ASSERT_PTR_NON_NULL(source, false);
    if (target == source) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Can't copy cell onto itself.");
        return false;
    }
    return cellCopy(target, source, true, 1, 1);
}


bool pmFPACopyStructure(pmFPA *target, const pmFPA *source, int xBin, int yBin)
{
    PS_ASSERT_PTR_NON_NULL(target, false);
    PS_ASSERT_PTR_NON_NULL(source, false);
    PS_ASSERT_INT_POSITIVE(xBin, false);
    PS_ASSERT_INT_POSITIVE(yBin, false);
    if (target == source) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Can't copy FPA onto itself.");
        return false;
    }
    return fpaCopy(target, source, false, xBin, yBin);
}

bool pmChipCopyStructure(pmChip *target, const pmChip *source, int xBin, int yBin)
{
    PS_ASSERT_PTR_NON_NULL(target, false);
    PS_ASSERT_PTR_NON_NULL(source, false);
    PS_ASSERT_INT_POSITIVE(xBin, false);
    PS_ASSERT_INT_POSITIVE(yBin, false);
    if (target == source) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Can't copy chip onto itself.");
        return false;
    }
    return chipCopy(target, source, false, xBin, yBin);
}

bool pmCellCopyStructure(pmCell *target, const pmCell *source, int xBin, int yBin)
{
    PS_ASSERT_PTR_NON_NULL(target, false);
    PS_ASSERT_PTR_NON_NULL(source, false);
    PS_ASSERT_INT_POSITIVE(xBin, false);
    PS_ASSERT_INT_POSITIVE(yBin, false);
    if (target == source) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Can't copy cell onto itself.");
        return false;
    }
    return cellCopy(target, source, false, xBin, yBin);
}
