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

#include <pslib.h>

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

#include "pmFPACopy.h"

//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// File-static functions
//////////////////////////////////////////////////////////////////////////////////////////////////////////////

// Bin a region down by specified factors in x and y
static void binRegion(psRegion *region, // Region to be binned
                      int xBin, int yBin// Binning in x and y
                     )
{
    assert(region);
    assert(xBin > 0);
    assert(yBin > 0);

    // Want to include the lower bound: 1 binned by 4 --> 0; 3 binned by 4 --> 0; 4 binned by 4 --> 1
    region->x0 = (int)(region->x0 / xBin);
    region->y0 = (int)(region->y0 / yBin);
    // Want to exclude the upper bound: 4 binned by 4 --> 1; 5 binned by 4 --> 2; 7 binned by 4 --> 2
    region->x1 = (int)((region->x1 + xBin - 1) / xBin);
    region->y1 = (int)((region->y1 + yBin - 1) / yBin);
}

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;
    }

    psError(PS_ERR_IO, true, "Unable to find PHU for target cell.\n");
    return NULL;
}

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

static bool cellCopy(pmCell *target,     // The target cell
                     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);

    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 mdok = true;                   // Status of MD lookup
    bool xFlip = false;                 // Switch parity in x?
    bool yFlip = false;                 // Switch parity in y?
    {
        int xParityTarget = psMetadataLookupS32(&mdok, target->concepts, "CELL.XPARITY"); // Target x parity
        if (mdok && (xParityTarget == 1 || xParityTarget == -1))
        {
            int xParitySource = psMetadataLookupS32(&mdok, source->concepts, "CELL.XPARITY"); // Source parity
            if (mdok && (xParitySource == 1 || xParitySource == -1) && xParityTarget != xParitySource) {
                xFlip = true;
            }
        }
        int yParityTarget = psMetadataLookupS32(&mdok, target->concepts, "CELL.YPARITY"); // Target y parity
        if (mdok && (yParityTarget == 1 || yParityTarget == -1))
        {
            int yParitySource = psMetadataLookupS32(&mdok, source->concepts, "CELL.YPARITY"); // Source parity
            if (mdok && (yParitySource == 1 || yParitySource == -1) && yParityTarget != yParitySource) {
                yFlip = true;
            }
        }
        psTrace(__func__, 3, "xFlip: %d; yFlip: %d\n", xFlip, yFlip);
    }

    if (pixels && (xBin != 1 || yBin != 1)) {
        psLogMsg(__func__, PS_LOG_WARN, "Unable to copy pixels if binning is set --- copy turned off.\n");
        pixels = false;
    }

    // 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
        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 image
        if (sourceReadout->image) {
            if (targetReadout->image) {
                psFree(targetReadout->image);
            }
            if (pixels) {
                targetReadout->image = psImageFlip(NULL, sourceReadout->image, xFlip, yFlip);
            } else {
                psImage *image = sourceReadout->image; // The source image
                long binnedCols = (image->numCols + xBin - 1) / xBin;
                long binnedRows = (image->numRows + yBin - 1) / yBin;
                targetReadout->image = psImageAlloc(binnedCols, binnedRows, image->type.type);
            }
        }

        // Copy mask
        if (sourceReadout->mask) {
            if (targetReadout->mask) {
                psFree(targetReadout->mask);
            }
            if (pixels) {
                targetReadout->mask = psImageFlip(NULL, sourceReadout->mask, xFlip, yFlip);
            } else {
                psImage *mask = sourceReadout->mask; // The source mask image
                long binnedCols = (mask->numCols + xBin - 1) / xBin;
                long binnedRows = (mask->numRows + yBin - 1) / yBin;
                targetReadout->mask = psImageAlloc(binnedCols, binnedRows, mask->type.type);
            }
        }

        // Copy weight
        if (sourceReadout->weight) {
            if (targetReadout->weight) {
                psFree(targetReadout->weight);
            }
            if (pixels) {
                targetReadout->weight = psImageFlip(NULL, sourceReadout->weight, xFlip, yFlip);
            } else {
                psImage *weight = sourceReadout->weight; // The source weight image
                long binnedCols = (weight->numCols + xBin - 1) / xBin;
                long binnedRows = (weight->numRows + yBin - 1) / yBin;
                targetReadout->weight = psImageAlloc(binnedCols, binnedRows, weight->type.type);
            }
        }

        // 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;          // Copy of the bias
            if (pixels) {
                biasCopy = psImageFlip(NULL, bias, xFlip, yFlip);
            } else {
                long binnedCols = (bias->numCols + xBin - 1) / xBin;
                long binnedRows = (bias->numRows + yBin - 1) / yBin;
                biasCopy = psImageAlloc(binnedCols, binnedRows, bias->type.type);
            }
            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") != 0 && strcmp(name, "CELL.BIASSEC") != 0 &&
                strcmp(name, "CELL.XPARITY") != 0 && strcmp(name, "CELL.YPARITY") != 0 &&
                strcmp(name, "CELL.X0") != 0 && strcmp(name, "CELL.Y0") != 0) {
            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.
    if (xBin != 1 || yBin != 1) {
        psRegion *trimsec = psMetadataLookupPtr(&mdok, target->concepts, "CELL.TRIMSEC"); // The trim section
        if (mdok && trimsec && !psRegionIsNaN(*trimsec)) {
            binRegion(trimsec, xBin, yBin);
        }
        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)) {
                    binRegion(biassec, xBin, yBin);
                }
            }
            psFree(biassecsIter);
        }
    }

    // Need to update CELL.X0 and CELL.Y0 if we flipped
    if (xFlip) {
        int xZero = psMetadataLookupS32(NULL, source->concepts, "CELL.X0"); // CELL.X0 from source
        int xParity = psMetadataLookupS32(NULL, target->concepts, "CELL.XPARITY"); // Parity in x
        int xBin = psMetadataLookupS32(NULL, source->concepts, "CELL.XBIN"); // CELL.XBIN from source
        pmReadout *readout = source->readouts->data[0]; // A representative readout
        psTrace(__func__, 3, "CELL.X0: Before: %d After: %d\n", xZero,
                xZero - (readout->image->numCols - 1) * xParity * xBin);
        psTrace(__func__, 9, "(xParity: %d xBin: %d numCols: %d)\n", xParity, xBin, readout->image->numCols);
        xZero -= (readout->image->numCols - 1) * xParity * xBin; // 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, target->concepts, "CELL.YPARITY"); // Parity in y
        int yBin = psMetadataLookupS32(NULL, source->concepts, "CELL.YBIN"); // Parity in y
        pmReadout *readout = source->readouts->data[0]; // A representative readout
        psTrace(__func__, 3, "CELL.Y0: Before: %d After: %d\n", yZero,
                yZero - (readout->image->numRows - 1) * yParity * yBin);
        psTrace(__func__, 9, "(yParity: %d yBin: %d numRows: %d)\n", yParity, yBin, readout->image->numRows);
        yZero -= (readout->image->numRows - 1) * yParity * yBin; // 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
    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 && !targetHDU->header) {
        pmHDU *sourceHDU = pmHDUFromCell(source); // The source HDU
        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 && !targetPHU->header) {
        pmHDU *sourcePHU = findBlankPHU(source); // The source PHU
        targetPHU->header = psMetadataCopy(targetPHU->header, sourcePHU->header);
    }

    target->data_exists = true;
    return true;
}

static bool chipCopy(pmChip *target,          // The target chip
                     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);

    psArray *targetCells = target->cells; // The target cells
    psArray *sourceCells = source->cells; // The source cells
    if (targetCells->n != sourceCells->n) {
        psError(PS_ERR_IO, true, "Number of source cells (%d) differs from the number of target cells (%d)\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
    psMetadataCopy(target->concepts, source->concepts);

    target->data_exists = true;
    return status;
}

static bool fpaCopy(pmFPA *target,            // The target FPA
                    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_IO, true, "Number of source chips (%d) differs from the number of target chips (%d)\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,            // The target FPA
               pmFPA *source             // The source FPA, to be copied
              )
{
    PS_ASSERT_PTR_NON_NULL(target, false);
    PS_ASSERT_PTR_NON_NULL(source, false);
    return fpaCopy(target, source, true, 1, 1);
}

bool pmChipCopy(pmChip *target,          // The target chip
                pmChip *source           // The source chip, to be copied
               )
{
    PS_ASSERT_PTR_NON_NULL(target, false);
    PS_ASSERT_PTR_NON_NULL(source, false);
    return chipCopy(target, source, true, 1, 1);
}

bool pmCellCopy(pmCell *target,          // The target cell
                pmCell *source           // The source cell, to be copied
               )
{
    PS_ASSERT_PTR_NON_NULL(target, false);
    PS_ASSERT_PTR_NON_NULL(source, false);
    return cellCopy(target, source, true, 1, 1);
}


bool pmFPACopyStructure(pmFPA *target,   // The target FPA
                        pmFPA *source,   // The source FPA, to be copied
                        int xBin, int yBin // Binning factors in x and y
                       )
{
    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);
    return fpaCopy(target, source, false, xBin, yBin);
}

bool pmChipCopyStructure(pmChip *target, // The target chip
                         pmChip *source, // The source chip, to be copied
                         int xBin, int yBin // Binning factors in x and y
                        )
{
    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);
    return chipCopy(target, source, false, xBin, yBin);
}

bool pmCellCopyStructure(pmCell *target, // The target cell
                         pmCell *source, // The source cell, to be copied
                         int xBin, int yBin // Binning factors in x and y
                        )
{
    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);
    return cellCopy(target, source, false, xBin, yBin);
}
