/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set sw=2 sts=2 et cin: */
/*
 * This file is part of the MUSE Instrument Pipeline
 * Copyright (C) 2005-2014 European Southern Observatory
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

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

/*----------------------------------------------------------------------------*
 *                              Includes                                      *
 *----------------------------------------------------------------------------*/
#include <stdio.h>
#include <float.h>
#include <math.h>
#include <string.h>
#include <cpl.h>
#include <muse.h>
#include "muse_wavecal_z.h"

/*---------------------------------------------------------------------------*
 *                             Functions code                                *
 *---------------------------------------------------------------------------*/

/*---------------------------------------------------------------------------*/
/**
  @brief    Sum the image list and save the resulting image.
  @param    aImages       the image list to combine
  @param    aProcessing   the processing structure
  @param    aIFU          the IFU/channel number
  @param    aSave         if the file should actually be written to disk
  @return   the combined image or NULL on error

  Take care that all headers are transferred / erased before saving the result.
 */
/*---------------------------------------------------------------------------*/
static muse_image *
muse_wavecal_sum_and_save(muse_imagelist *aImages, muse_processing *aProcessing,
                          int aIFU, cpl_boolean aSave)
{
  cpl_ensure(aImages && aProcessing, CPL_ERROR_NULL_INPUT, NULL);

  /* do the final (sum!) image combination if requested */
  char *pname = cpl_sprintf("muse.%s.combine", aProcessing->name);
  cpl_parameter *param = cpl_parameterlist_find(aProcessing->parameters, pname);
  char *porig = cpl_strdup(cpl_parameter_get_string(param));
  cpl_parameter_set_string(param, "sum");
  cpl_free(pname);
  muse_combinepar *cpars = muse_combinepar_new(aProcessing->parameters,
                                               "muse.muse_wavecal");
  cpl_parameter_set_string(param, porig);
  cpl_free(porig);
  muse_image *image = muse_combine_images(cpars, aImages);
  muse_combinepar_delete(cpars);
  if (!image) {
    cpl_error_set_message(__func__, CPL_ERROR_ILLEGAL_OUTPUT,
                          "summing per-lamp images did not work");
    return NULL;
  }

  /* transfer NSATURATION headers from lamp-combined images to final combined image */
  /* loop over lamps (entries in the imagelist) */
  unsigned int k;
  for (k = 0; k < muse_imagelist_get_size(aImages); k++) {
    cpl_errorstate state = cpl_errorstate_get();
    unsigned int i = 1;
    while (cpl_errorstate_is_equal(state)) {
      char *kwget = cpl_sprintf(QC_WAVECAL_PREFIXi" "QC_BASIC_NSATURATED, i),
           *kwout = cpl_sprintf(QC_WAVECAL_PREFIXli" "QC_BASIC_NSATURATED, k+1, i);
      /* this will change the errorstate if the keyword doesn't exist: */
      int nsaturated = cpl_propertylist_get_int(muse_imagelist_get(aImages, k)->header,
                                                kwget);
      if (cpl_errorstate_is_equal(state)) {
        cpl_propertylist_update_int(image->header, kwout, nsaturated);
      }
      cpl_free(kwget);
      cpl_free(kwout);
      i++;
    } /* while (errorstate is the same) */
    /* reset the old errorstate, we don't want to leak *
     * it outside, since it's not a real error         */
    cpl_errorstate_set(state);
  } /* for k (all images in list) */
  /* count saturated pixels in final combined image */
  muse_basicproc_qc_saturated(image, QC_WAVECAL_PREFIX);

  /* save the pre-processed image to file                                *
   * (this should be done, even if the wavelength calibration fails so   *
   * that the user can check what input files were used for the wavecal) */
  if (aSave) {
    muse_processing_save_image(aProcessing, aIFU, image, MUSE_TAG_ARC_RED);
  }

  return image;
} /* muse_wavecal_sum_and_save() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Transfer a QC parameters related to saturation from one header.
  @param   aHeader   target of the transferred QC keywords
  @param   aHin      source header of the transferred QC keyword
  @param   aPrefix   prefix to search the QC parameters
  @param   aIdx      index of the image in the image list, or 99 for master

  Postfix the QC parameter name with the respective lamp number (or zero, if we
  only have a master frame).
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
muse_wavecal_qc_nsaturated_single(cpl_propertylist *aHeader,
                                  cpl_propertylist *aHin,
                                  const char *aPrefix, unsigned int aIdx)
{
  cpl_ensure_code(aHeader && aHin, CPL_ERROR_NULL_INPUT);

  /* set up lamp name(s) and number(s), depending on master status or header */
  char *name = NULL;
  cpl_array *numbers = NULL;
  if (aIdx == 99) {
    name = cpl_sprintf("combined");
    numbers = cpl_array_new(1, CPL_TYPE_INT);
    cpl_array_set_int(numbers, 0, 99);
  } else {
    name = muse_utils_header_get_lamp_names(aHin, ',');
    numbers = muse_utils_header_get_lamp_numbers(aHin);
  }

  /* use the info to actually propagate the nsaturated pixel info, *
   * if only a single lamp was found active in the header          */
  if (cpl_array_get_size(numbers) != 1) {
    cpl_msg_warning(__func__, "Image %u: %"CPL_SIZE_FORMAT" lamps active (%s)",
                    aIdx, cpl_array_get_size(numbers), name);
  } else {
    int nlamp = cpl_array_get_int(numbers, 0, NULL);
    char *kw = NULL, /* output keyword */
         *kwin = NULL;
    if (aPrefix[strlen(aPrefix)-1] == ' ') {
      kw = cpl_sprintf("%s%s LAMP%d", aPrefix, QC_BASIC_NSATURATED, nlamp);
      kwin = cpl_sprintf("%s%s", aPrefix, QC_BASIC_NSATURATED);
    } else {
      kw = cpl_sprintf("%s %s LAMP%d", aPrefix, QC_BASIC_NSATURATED, nlamp);
      kwin = cpl_sprintf("%s %s", aPrefix, QC_BASIC_NSATURATED);
    }
    int nsaturated = cpl_propertylist_get_int(aHin, kwin);
    cpl_free(kwin);
    cpl_msg_debug(__func__, "Lamp %d (%s, image %u): %d saturated pixels",
                  nlamp, name, aIdx, nsaturated);
    cpl_propertylist_update_int(aHeader, kw, nsaturated);
    cpl_free(kw);
  } /* else: one single lamp */
  cpl_array_delete(numbers);
  cpl_free(name);

  return CPL_ERROR_NONE;
} /* muse_wavecal_qc_nsaturated_single() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Transfer QC parameters related to saturation to a given header.
  @param   aHeader   target of the transferred QC keywords
  @param   aList     possible source of the individual saturation counts
  @param   aMaster   possible source of a combined saturation count
  @param   aPrefix   prefix to search the QC parameters

  Postfix the QC parameter name with the respective lamp number (or zero, if we
  only have a master frame).
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
muse_wavecal_qc_nsaturated(cpl_propertylist *aHeader, muse_imagelist *aList,
                           muse_image *aMaster, const char *aPrefix)
{
  cpl_ensure_code(aHeader, CPL_ERROR_NULL_INPUT);

  /* copy the saturated pixel counts from the fully *
   * combined master image, if it exists            */
  if (aMaster && aMaster->header) {
    muse_wavecal_qc_nsaturated_single(aHeader, aMaster->header, aPrefix, 99);
  }

  /* now copy the saturated pixel counts from the *
   * (lamp-combined) single images, if they exist */
  unsigned int k;
  for (k = 0; aList && k < aList->size; k++) {
    cpl_propertylist *header = muse_imagelist_get(aList, k)->header;
    muse_wavecal_qc_nsaturated_single(aHeader, header, aPrefix, k);
  } /* for k (all images) */
  return CPL_ERROR_NONE;
} /* muse_wavecal_qc_nsaturated() */

/*---------------------------------------------------------------------------*/
/**
  @brief    Interpret the command line options and execute the data processing
  @param    aProcessing   the processing structure
  @param    aParams       the parameters list
  @return   0 if everything is ok, -1 something went wrong
 */
/*---------------------------------------------------------------------------*/
int
muse_wavecal_compute(muse_processing *aProcessing,
                     muse_wavecal_params_t *aParams)
{
  muse_wave_weighting_type fweight = MUSE_WAVE_WEIGHTING_UNIFORM;
  if (aParams->fitweighting == MUSE_WAVECAL_PARAM_FITWEIGHTING_CERR) {
    fweight = MUSE_WAVE_WEIGHTING_CERR;
  } else if (aParams->fitweighting == MUSE_WAVECAL_PARAM_FITWEIGHTING_SCATTER) {
    fweight = MUSE_WAVE_WEIGHTING_SCATTER;
  } else if (aParams->fitweighting == MUSE_WAVECAL_PARAM_FITWEIGHTING_CERRSCATTER) {
    fweight = MUSE_WAVE_WEIGHTING_CERRSCATTER;
  } else if (aParams->fitweighting != MUSE_WAVECAL_PARAM_FITWEIGHTING_UNIFORM) {
    cpl_msg_error(__func__, "unknown fitweighting method \"%s\"",
                  aParams->fitweighting_s);
    return -1;
  }

  muse_basicproc_params *bpars = muse_basicproc_params_new(aProcessing->parameters,
                                                           "muse.muse_wavecal");
  cpl_frameset **labeledframes = NULL;
  muse_imagelist *images = muse_basicproc_combine_images_lampwise(aProcessing,
                                                                  aParams->nifu,
                                                                  bpars,
                                                                  &labeledframes);
  muse_basicproc_params_delete(bpars);
  cpl_ensure(images, cpl_error_get_code(), -1);
  /* save all resulting images, but only if there were different lamp *
   * setups; otherwise we only save the combined image, see below     */
  cpl_frameset *used = aProcessing->usedframes;
  unsigned int k, kimages = muse_imagelist_get_size(images);
  cpl_boolean needsum = !aParams->lampwise || aParams->resample;
  for (k = 0; k < kimages; k++) {
    /* only save individual files if different from combined one */
    if (!(kimages == 1 && needsum)) {
      muse_image *image = muse_imagelist_get(images, k);
      if (labeledframes) { /* replace usedframes with ones used for _this_ image */
        aProcessing->usedframes = labeledframes[k];
      }
      muse_basicproc_qc_saturated(image, QC_WAVECAL_PREFIX);
      cpl_propertylist_erase_regexp(image->header, MUSE_WCS_KEYS, 0);
      if (aParams->saveimages) {
        muse_processing_save_image(aProcessing, aParams->nifu, image,
                                   MUSE_TAG_ARC_RED_LAMP);
      }
    }
    /* delete the corresponding frameset in any case, if present */
    if (labeledframes) {
      cpl_frameset_delete(labeledframes[k]);
    }
  } /* for k (all images in list) */
  cpl_free(labeledframes);
  aProcessing->usedframes = used;
  muse_image *masterimage = NULL;
  if (needsum) {
    masterimage = muse_wavecal_sum_and_save(images, aProcessing, aParams->nifu,
                                            aParams->saveimages);
  } /* if !lampwise */

  /* line catalog is needed in any case */
  muse_table *linelist = muse_processing_load_table(aProcessing,
                                                    MUSE_TAG_LINE_CATALOG, 0);
  int rc = muse_wave_lines_check(linelist) ? 0 : -1;
  if (rc) {
    muse_table_delete(linelist);
    muse_image_delete(masterimage);
    muse_imagelist_delete(images);
    return rc;
  }
  /* trace table is needed in any case if we made it here */
  cpl_table *tracetable = muse_processing_load_ctable(aProcessing, MUSE_TAG_TRACE_TABLE,
                                                      aParams->nifu);

  /* the main step: compute the wavelength solution, add QC parameters   *
   * to the FITS headers of the input exposure(s), since the output      *
   * table doesn't carry headers, and create an output header for saving *
   * by copying most of the input headers.                               */
  cpl_table *wavecaltable = NULL;
  cpl_propertylist *header = NULL;
  muse_wave_params *p = muse_wave_params_new(muse_imagelist_get(images, 0)->header);
  p->xorder = aParams->xorder;
  p->yorder = aParams->yorder;
  p->detsigma = aParams->sigma;
  p->ddisp = aParams->dres;
  p->tolerance = aParams->tolerance;
  p->linesigma = aParams->linesigma;
  p->rflag = aParams->residuals; /* the residuals component itself stays NULL */
  p->fitsigma = aParams->fitsigma;
  p->fitweighting = fweight;
  if (aParams->lampwise) { /* separate image handling (new method) */
    wavecaltable = muse_wave_calib_lampwise(images, tracetable, linelist->table,
                                            p);
    header = cpl_propertylist_duplicate(muse_imagelist_get(images, 0)->header);
  } else { /* direct measurement and fit on single combined image (old method) */
    wavecaltable = muse_wave_calib(masterimage, tracetable, linelist->table, p);
    header = cpl_propertylist_duplicate(masterimage->header);
  }
  muse_table_delete(linelist);
  if (!wavecaltable) { /* failure of the main step, return now */
    muse_image_delete(masterimage);
    muse_imagelist_delete(images);
    cpl_table_delete(tracetable);
    cpl_propertylist_delete(header);
    muse_wave_params_delete(p);
    return -1;
  }

  /* propagate saturated pixel counts of (the) combined image(s), *
   * but remove old ones                                          */
  muse_wavecal_qc_nsaturated(header, images, masterimage, QC_WAVECAL_PREFIX);
  cpl_propertylist_erase_regexp(header, "^ESO QC.*"QC_BASIC_NSATURATED"$", 0);
  /* create table header by copying most of the image header *
   * and save the wavelength solution in this header         */
  cpl_propertylist_erase_regexp(header,
                                "^SIMPLE$|^BITPIX$|^NAXIS|^EXTEND$|^XTENSION$|"
                                "^DATASUM$|^DATAMIN$|^DATAMAX$|^DATAMD5$|"
                                "^PCOUNT$|^GCOUNT$|^HDUVERS$|^BLANK$|"
                                "^BZERO$|^BSCALE$|^BUNIT$|^CHECKSUM$|^INHERIT$|"
                                "^PIPEFILE$|^ESO PRO ", 0);
  muse_processing_save_table(aProcessing, aParams->nifu, wavecaltable, header,
                             MUSE_TAG_WAVECAL_TABLE, MUSE_TABLE_TYPE_CPL);
  if (p->residuals) {
    /* don't want to save QC headers with the residuals table: */
    cpl_propertylist_erase_regexp(header, QC_WAVECAL_PREFIX, 0);
    muse_processing_save_table(aProcessing, aParams->nifu, p->residuals, header,
                               MUSE_TAG_WAVECAL_DEBUG, MUSE_TABLE_TYPE_CPL);
  }
  muse_wave_params_delete(p);
  cpl_propertylist_delete(header);

  /* now the extra steps: resampling, and/or wavemap creation */
  if (aParams->resample) {
    /* to visualize wavelength calibration, use the     *
     * pixel table to resample the data onto a 2D image */
    muse_pixtable *pixtable = muse_pixtable_create(masterimage, tracetable,
                                                   wavecaltable, NULL);
    muse_image *resampled = muse_resampling_image(pixtable,
                                                  MUSE_RESAMPLE_WEIGHTED_RENKA,
                                                  1.0, 1.25);
    muse_pixtable_delete(pixtable);
    if (resampled) {
      /* don't want to save QC headers with the resampled image: */
      cpl_propertylist_erase_regexp(resampled->header, QC_WAVECAL_PREFIX, 0);
      muse_processing_save_cimage(aProcessing, aParams->nifu, resampled->data,
                                  resampled->header, MUSE_TAG_ARC_RESAMP);
      muse_image_delete(resampled);
    }
  } /* if resample */

  if (aParams->wavemap) {
    muse_image *image = masterimage ? masterimage
                                    : muse_imagelist_get(images, 0);
    cpl_image *map = muse_wave_map(image, wavecaltable, tracetable);
    /* simply use the header of the incoming image */
    muse_processing_save_cimage(aProcessing, aParams->nifu, map, image->header,
                                MUSE_TAG_WAVE_MAP);
    cpl_image_delete(map);
  } /* if wavemap */

  /* clean up */
  cpl_table_delete(wavecaltable);
  muse_image_delete(masterimage);
  muse_imagelist_delete(images);
  cpl_table_delete(tracetable);

  return rc; /* can only be 0 or -1 */
} /* muse_wavecal_compute() */
