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

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

#include "pmConfig.h"
#include "pmDetrendDB.h"

#include "pmHDU.h"
#include "pmFPA.h"
#include "pmFPALevel.h"
#include "pmFPARead.h"
#include "pmFPAWrite.h"
#include "pmFPAMaskWeight.h"
#include "pmFPAview.h"
#include "pmFPAfile.h"
#include "pmFPAfileFitsIO.h"
#include "pmFPACopy.h"
#include "pmFPAConstruct.h"
#include "pmDark.h"

pmFPA *pmFPAfileSuitableFPA(const pmFPAfile *file, const pmFPAview *view, const pmConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(file, NULL);
    PS_ASSERT_PTR_NON_NULL(view, NULL);

    if (!file->format) {                // Working with the same output format as input format
        return psMemIncrRefCounter(file->fpa);
    }

    // May need to change format
    pmFPALevel level = pmFPAviewLevel(view); // Level for the view
    if (level == PM_FPA_LEVEL_NONE || level == PM_FPA_LEVEL_READOUT) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
                "This function shouldn't be called at the readout (or unknown) level.");
        return NULL;
    }

    // Does the HDU of interest conform to the desired format?
    pmHDU *hdu = pmFPAviewThisHDU(view, file->fpa); // The HDU of interest
    if (hdu && hdu->format == file->format) {
        // No work required
        return psMemIncrRefCounter(file->fpa);
    }

    // Otherwise, we have to generate a copy with the correct format

    pmFPAview *phuView = pmFPAviewAlloc(0); // View corresponding to the PHU
    *phuView = *view;               // Copy contents
    pmFPALevel phuLevel = pmFPAPHULevel(file->format); // Level for the PHU
    switch (phuLevel) {
      case PM_FPA_LEVEL_FPA:
        phuView->chip = -1;
        // Flow through
      case PM_FPA_LEVEL_CHIP:
        phuView->cell = -1;
        // Flow through
      case PM_FPA_LEVEL_CELL:
        phuView->readout = -1;
        break;
      case PM_FPA_LEVEL_READOUT:
      case PM_FPA_LEVEL_NONE:
      default:
        psAbort("Should never get here: bad phu level.\n");
    }

    pmFPA *nameSource = file->src; // Source of FPA.NAME
    if (!nameSource) {
        nameSource = file->fpa;
    }
    bool mdok;                  // Status of MD lookup
    const char *fpaname = psMetadataLookupStr(&mdok, nameSource->concepts, "FPA.NAME"); // Name of FPA

    pmFPA *copy = pmFPAConstruct(file->camera);  // FPA to return
    if (!pmFPAAddSourceFromView(copy, fpaname, phuView, file->format)) {
        psError(PS_ERR_UNKNOWN, false, "Unable to insert HDU into FPA for writing.\n");
        psFree(copy);
        psFree(phuView);
        return NULL;
    }
    psFree(phuView);

    switch (level) {
      case PM_FPA_LEVEL_FPA:
        if (!pmFPACopy(copy, file->fpa)) {
            psError(PS_ERR_UNKNOWN, false, "Unable to copy FPA for format conversion.\n");
            return NULL;
        }
        return copy;
      case PM_FPA_LEVEL_CHIP: {
          pmChip *chip = pmFPAviewThisChip(view, copy); // Chip of interest
          pmChip *srcChip = pmFPAviewThisChip(view, file->fpa); // Source chip
          if (!pmChipCopy(chip, srcChip)) {
              psError(PS_ERR_UNKNOWN, false, "Unable to copy chip for format conversion.\n");
              return false;
          }
          return copy;
      }
      case PM_FPA_LEVEL_CELL: {
          pmCell *cell = pmFPAviewThisCell(view, copy); // Cell of interest
          pmCell *srcCell = pmFPAviewThisCell(view, file->fpa); // Source cell
          if (!pmCellCopy(cell, srcCell)) {
              psError(PS_ERR_UNKNOWN, false, "Unable to copy cell for format conversion.\n");
              return false;
          }
          return copy;
      }
      case PM_FPA_LEVEL_READOUT:
      case PM_FPA_LEVEL_NONE:
      default:
        psAbort("Should never get here: bad phu level.\n");
    }

    return NULL;

}

// given an already-opened fits file, read the table corresponding to the specified view
bool pmFPAviewReadFitsTable(const pmFPAview *view, pmFPAfile *file, const char *name)
{
    PS_ASSERT_PTR_NON_NULL(view, false);
    PS_ASSERT_PTR_NON_NULL(file, false);

    pmFPA *fpa = file->fpa;             // FPA of interest
    psFits *fits = file->fits;          // FITS file

    if (view->chip == -1) {
        return pmFPAReadTable(fpa, fits, name) > 0;
    }

    if (view->cell == -1) {
        pmChip *chip = pmFPAviewThisChip(view, fpa); // Chip of interest
        return pmChipReadTable(chip, fits, name) > 0;
    }

    pmCell *cell = pmFPAviewThisCell(view, fpa); // Cell of interest
    return pmCellReadTable(cell, fits, name) > 0;
}

// given an already-opened fits file, write the table corresponding to the specified view
bool pmFPAviewWriteFitsTable(const pmFPAview *view, pmFPAfile *file, const char *name)
{
    PS_ASSERT_PTR_NON_NULL(view, false);
    PS_ASSERT_PTR_NON_NULL(file, false);

    pmFPA *fpa = file->fpa;             // FPA of interest
    psFits *fits = file->fits;          // FITS file

    if (view->chip == -1) {
        return pmFPAWriteTable(fits, fpa, name) > 0;
    }

    if (view->cell == -1) {
        pmChip *chip = pmFPAviewThisChip(view, fpa); // Chip of interest
        return pmChipWriteTable(fits, chip, name) > 0;
    }

    pmCell *cell = pmFPAviewThisCell(view, fpa); // Cell of interest
    return pmCellWriteTable(fits, cell, name) > 0;
}


// given an already-opened fits file, read the components corresponding to the specified view
static bool fpaViewReadFitsImage(const pmFPAview *view, // FPA view, specifying the level of interest
                                 pmFPAfile *file, // FPA file of interest
                                 bool (*fpaReadFunc)(pmFPA*, psFits*, psDB*), // Function to read FPA
                                 bool (*chipReadFunc)(pmChip*, psFits*, psDB*), // Function to read chip
                                 bool (*cellReadFunc)(pmCell*, psFits*, psDB*) // Function to read cell
                                )
{
    assert(view);
    assert(file);

    pmFPA *fpa = file->fpa;             // FPA of interest
    psFits *fits = file->fits;          // FITS file from which to read

    if (view->chip == -1) {
        return fpaReadFunc(fpa, fits, NULL);
    }

    if (view->chip >= fpa->chips->n) {
        psError(PS_ERR_IO, true, "Requested chip == %d >= fpa->chips->n == %ld", view->chip, fpa->chips->n);
        return false;
    }
    pmChip *chip = fpa->chips->data[view->chip]; // Chip of interest

    if (view->cell == -1) {
        return chipReadFunc(chip, fits, NULL);
    }

    if (view->cell >= chip->cells->n) {
        psError(PS_ERR_IO, true, "Requested cell == %d >= chip->cells->n == %ld", view->cell, chip->cells->n);
        return false;
    }
    pmCell *cell = chip->cells->data[view->cell]; // Cell of interest

    if (view->readout == -1) {
        return cellReadFunc(cell, fits, NULL);
    }
    psError(PS_ERR_UNKNOWN, true, "Bad view: %d,%d", view->chip, view->cell);
    return false;

    // XXX pmReadoutRead, pmReadoutReadSegement disabled for now
    #if 0

    if (view->readout >= cell->readouts->n) {
        psError(PS_ERR_IO, true, "Requested readout == %d >= cell->readouts->n == %d",
                view->readout, cell->readouts->n);
        return false;
    }
    pmReadout *readout = cell->readouts->data[view->readout];

    if (view->nRows == 0) {
        pmReadoutRead (readout, fits, NULL);
    } else {
        pmReadoutReadSegment (readout, fits, view->nRows, view->iRows, NULL, NULL);
    }
    return true;
    #endif
}


bool pmFPAviewReadFitsImage(const pmFPAview *view, pmFPAfile *file)
{
    PS_ASSERT_PTR_NON_NULL(view, false);
    PS_ASSERT_PTR_NON_NULL(file, false);
    return fpaViewReadFitsImage(view, file, pmFPARead, pmChipRead, pmCellRead);
}

bool pmFPAviewReadFitsMask(const pmFPAview *view, pmFPAfile *file)
{
    PS_ASSERT_PTR_NON_NULL(view, false);
    PS_ASSERT_PTR_NON_NULL(file, false);
    return fpaViewReadFitsImage(view, file, pmFPAReadMask, pmChipReadMask, pmCellReadMask);
}

bool pmFPAviewReadFitsWeight(const pmFPAview *view, pmFPAfile *file)
{
    PS_ASSERT_PTR_NON_NULL(view, false);
    PS_ASSERT_PTR_NON_NULL(file, false);
    return fpaViewReadFitsImage(view, file, pmFPAReadWeight, pmChipReadWeight, pmCellReadWeight);
}

bool pmFPAviewReadFitsDark(const pmFPAview *view, pmFPAfile *file)
{
    PS_ASSERT_PTR_NON_NULL(view, false);
    PS_ASSERT_PTR_NON_NULL(file, false);
    return fpaViewReadFitsImage(view, file, pmFPAReadDark, pmChipReadDark, pmCellReadDark);
}

bool pmFPAviewReadFitsHeaderSet(const pmFPAview *view, pmFPAfile *file)
{
    PS_ASSERT_PTR_NON_NULL(view, false);
    PS_ASSERT_PTR_NON_NULL(file, false);
    return fpaViewReadFitsImage(view, file, pmFPAReadHeaderSet, pmChipReadHeaderSet, pmCellReadHeaderSet);
}

// given an already-opened fits file, write the components corresponding
// to the specified view. when the file was opened, pmFPA/Chip/CellWrite was
// called on it with blank=true to write the (possible) blank PHU
// do NOT call the functions below with blank=true or they will write
// out data in an inconsistent fashion
// the calls below should recurse down the element to write out all components.
static bool fpaViewWriteFitsImage(const pmFPAview *view, // FPA view, specifying the level of interest
                                  pmFPAfile *file, // FPA file of interest
                                  const pmConfig *config, // Configuration
                                  bool (*fpaWriteFunc)(pmFPA*, psFits*, psDB*, bool, bool), // Func for FPA
                                  bool (*chipWriteFunc)(pmChip*, psFits*, psDB*, bool, bool), // Func for chip
                                  bool (*cellWriteFunc)(pmCell*, psFits*, psDB*, bool) // Func for cell
                                 )
{
    assert(view);
    assert(file);

    psFits *fits = file->fits;          // FITS file
    PS_ASSERT_PTR_NON_NULL(fits, false);

    pmFPA *fpa = pmFPAfileSuitableFPA(file, view, config); // FPA to write

    switch (pmFPAviewLevel(view)) {
    case PM_FPA_LEVEL_FPA: {
            bool success = fpaWriteFunc(fpa, fits, NULL, false, true);
            psFree(fpa);
            return success;
        }
    case PM_FPA_LEVEL_CHIP: {
            pmChip *chip = pmFPAviewThisChip(view, fpa); // Chip of interest
            bool success = chipWriteFunc(chip, fits, NULL, false, true);
            psFree(fpa);
            return success;
        }
    case PM_FPA_LEVEL_CELL: {
            pmCell *cell = pmFPAviewThisCell(view, fpa); // Cell of interest
            bool success = cellWriteFunc(cell, fits, NULL, false);
            psFree(fpa);
            return success;
        }
    case PM_FPA_LEVEL_READOUT:
        #if 0 // XXX disable readout write for now

        {
            pmReadout *readout = pmFPAviewThisReadout(view, file->fpa); // Readout of interest
            if (changeFormat)
        {
            // No copy function defined for readouts!
            psError(PS_ERR_UNKNOWN, false, "Unable to copy readout for format conversion on write.\n");
                return false;
            }
            if (view->nRows == 0)
        {
            return pmReadoutWrite(readout, fits, NULL, NULL);
            } else
            {
                return pmReadoutWriteSegment(readout, fits, view->nRows, view->iRows, NULL, NULL);
            }
        }
        #endif
    case PM_FPA_LEVEL_NONE:
    default:
        psAbort("Should never reach here: invalid file level.");
    }

    return false;
}

bool pmFPAviewWriteFitsImage(const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(view, false);
    PS_ASSERT_PTR_NON_NULL(file, false);
    return fpaViewWriteFitsImage(view, file, config, pmFPAWrite, pmChipWrite, pmCellWrite);
}

bool pmFPAviewWriteFitsMask(const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(view, false);
    PS_ASSERT_PTR_NON_NULL(file, false);
    return fpaViewWriteFitsImage(view, file, config, pmFPAWriteMask, pmChipWriteMask, pmCellWriteMask);
}

bool pmFPAviewWriteFitsWeight(const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(view, false);
    PS_ASSERT_PTR_NON_NULL(file, false);
    return fpaViewWriteFitsImage(view, file, config, pmFPAWriteWeight, pmChipWriteWeight, pmCellWriteWeight);
}

bool pmFPAviewWriteFitsDark(const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(view, false);
    PS_ASSERT_PTR_NON_NULL(file, false);
    return fpaViewWriteFitsImage(view, file, config, pmFPAWriteDark, pmChipWriteDark, pmCellWriteDark);
}

// given an already-opened fits file, read the components corresponding
// to the specified view
bool pmFPAviewFreeData(const pmFPAview *view, pmFPAfile *file)
{
    PS_ASSERT_PTR_NON_NULL(view, false);
    PS_ASSERT_PTR_NON_NULL(file, false);

    pmFPA *fpa = file->fpa;

    if (view->chip == -1) {
        psTrace ("pmFPAfile", 5, "freeing fpa for %s\n", file->filename);
        pmFPAFreeData (fpa);
        // XXX drop me: file->fpa = NULL;
        return true;
    }

    if (view->chip >= fpa->chips->n) {
        psError(PS_ERR_IO, true, "Requested chip == %d >= fpa->chips->n == %ld", view->chip, fpa->chips->n);
        return false;
    }
    pmChip *chip = fpa->chips->data[view->chip];

    if (view->cell == -1) {
        psTrace ("pmFPAfile", 5, "freeing chip %d for %s\n", view->chip, file->filename);
        pmChipFreeData (chip);
        return true;
    }

    if (view->cell >= chip->cells->n) {
        psError(PS_ERR_IO, true, "Requested cell == %d >= chip->cells->n == %ld", view->cell, chip->cells->n);
        return false;
    }
    pmCell *cell = chip->cells->data[view->cell];

    if (view->readout == -1) {
        psTrace ("pmFPAfile", 5, "freeing cell %d for %s\n", view->cell, file->filename);
        pmCellFreeData (cell);
        return true;
    }
    psError(PS_ERR_UNKNOWN, true, "Returning false");
    return false;

    // XXX pmReadoutRead, pmReadoutReadSegement disabled for now
    #if 0

    if (view->readout >= cell->readouts->n) {
        psError(PS_ERR_IO, true, "Requested readout == %d >= cell->readouts->n == %d",
                view->readout, cell->readouts->n);
        return false;
    }
    pmReadout *readout = cell->readouts->data[view->readout];

    if (view->nRows == 0) {
        pmReadoutRead (readout, fits, NULL);
    } else {
        pmReadoutReadSegment (readout, fits, view->nRows, view->iRows, NULL, NULL);
    }
    return true;
    #endif
}

#if 0
// Shouldn't need this --- when we want to free fringe data, we want to free the whole level, not just the
// table.

// Free the table within a cell
static void freeTable(pmCell *cell,     // Cell of interest
                      const char *name  // Name of table to free
                     )
{
    assert(cell);
    assert(name && strlen(name) > 0);

    psString headerName = NULL;         // Name of header
    psStringAppend(&headerName, "%s.HEADER", name);
    if (psMetadataLookup(cell->analysis, headerName)) {
        psMetadataRemoveKey(cell->analysis, headerName);
    }
    psFree(headerName);

    if (psMetadataLookup(cell->analysis, name)) {
        psMetadataRemoveKey(cell->analysis, name);
    }

    return;
}

// given a file, free the components corresponding to the specified view
bool pmFPAviewFreeFitsTable (const pmFPAview *view, pmFPAfile *file, const char *name)
{
    PS_ASSERT_PTR_NON_NULL(view, false);
    PS_ASSERT_PTR_NON_NULL(file, false);

    pmFPA *fpa = file->fpa;

    if (view->chip == -1) {
        psArray *chips = fpa->chips;    // Array of chips
        for (int i = 0; i < chips->n; i++) {
            pmChip *chip = chips->data[i]; // Chip of interest
            psArray *cells = chip->cells; // Array of cells
            for (int j = 0; j < cells->n; j++) {
                pmCell *cell = cells->data[j]; // Cell of interest
                freeTable(cell, name);
            }
        }
        return true;
    }

    if (view->cell == -1) {
        pmChip *chip = pmFPAviewThisChip(view, fpa); // Chip of interest
        psArray *cells = chip->cells;   // Array of cells
        for (int i = 0; i < cells->n; i++) {
            pmCell *cell = cells->data[i]; // Cell of interest
            freeTable(cell, name);
        }
        return true;
    }

    pmCell *cell = pmFPAviewThisCell(view, fpa); // Cell of interest
    freeTable(cell, name);
    return true;
}

#endif

bool pmFPAviewFitsWritePHU (const pmFPAview *view, pmFPAfile *file, const pmConfig *config) {

    bool status = false;

    if (file->mode != PM_FPA_MODE_WRITE) return true;
    if (file->wrote_phu) return true;

    // select or generate the desired fpa in the correct output format
    pmFPA *fpa = pmFPAfileSuitableFPA(file, view, config);
    pmHDU *phu = pmFPAviewThisHDU(view, fpa);
    if (!phu || !phu->blankPHU) {
        // No PHU to write!
        psFree(fpa);
        return true;
    }

    switch (file->fileLevel) {
      case PM_FPA_LEVEL_FPA:
        status = pmFPAWrite(fpa, file->fits, NULL, true, false);
        break;
      case PM_FPA_LEVEL_CHIP: {
          pmChip *chip = pmFPAviewThisChip(view, fpa);
          status = pmChipWrite(chip, file->fits, NULL, true, false);
          break;
      }
      case PM_FPA_LEVEL_CELL: {
          pmCell *cell = pmFPAviewThisCell(view, fpa);
          status = pmCellWrite(cell, file->fits, NULL, true);
          break;
      }
      default:
        psAbort("fileLevel not correctly set");
        break;
    }

    if (!status) {
        psError(PS_ERR_UNKNOWN, false, "failed to write PHU for Image %s (%s)\n", file->filename, file->name);
        return false;
    }

    psFree(fpa);
    file->wrote_phu = true;
    return true;
}
