/* -*- 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) 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 <string.h>
#include <math.h>

#include <muse.h>
#include <muse_data_format_z.h>
#include <muse_instrument.h>
#include <muse_lsf.h>
#include "muse_lsf_z.h"

#ifndef USE_LSF_PARAMS
#ifdef MUSE_LSF_PARAM_METHOD_HERMIT
#error Please remove the "method" parameter from muse_lsf_params.xml
#endif
#endif

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

/**
   @brief Create the LSF of all slices of one IFU.
   @param aPixtable   special pixel table with arc lines
   @param aParams     the parameters list
   @return the LSF cube or NULL on error

   This loops over all slices and calls muse_lsf_fit_slice() for each.
 */
static muse_lsf_cube *
muse_lsf_compute_slices(muse_pixtable *aPixtable, muse_lsf_params_t *aParams)
{
  cpl_ensure(aPixtable, CPL_ERROR_NULL_INPUT, NULL);
  muse_lsf_cube *lsfCube = muse_lsf_cube_new(aParams->lsf_range,
                                             aParams->lsf_size,
                                             aParams->lambda_size,
                                             aPixtable->header);
  muse_pixtable **slice_pixtable = muse_pixtable_extracted_get_slices(aPixtable);

  int i_slice, n_slices = muse_pixtable_extracted_get_size(slice_pixtable);
#pragma omp parallel for default(none)               /* as req. by Ralf */ \
  shared(lsfCube, n_slices, slice_pixtable, aParams)
  for (i_slice = 0; i_slice < n_slices; i_slice++) {
    cpl_image *img = cpl_imagelist_get(lsfCube->img, i_slice);
    muse_lsf_fit_slice(slice_pixtable[i_slice], img,
                       lsfCube->wcs, aParams->lsf_regression_window);
  }
  muse_pixtable_extracted_delete(slice_pixtable);
  return lsfCube;
}

/**
   @brief Subtract the LSF from an arc pixtable
   @param aLsfCube    LSF datacube
   @param aPixtable   special pixel table with arc lines

   This function is for debugging/QA only and works only with arc
   pixel tables. The pixtable needs the additional columns
   <tt>line_lambda</tt> and <tt>line_flux</tt> to work.
 */
static cpl_error_code
muse_lsf_subtract_arcs(muse_lsf_cube *aLsfCube, cpl_table *aPixtable) {
  cpl_size n_rows = cpl_table_get_nrow(aPixtable);
  cpl_size i_row;
  for (i_row = 0; i_row < n_rows; i_row++) {
    uint32_t origin = (uint32_t)cpl_table_get_int(aPixtable, MUSE_PIXTABLE_ORIGIN,
                                                  i_row, NULL);
    int i_slice = muse_pixtable_origin_get_slice(origin);
    double lambda = cpl_table_get(aPixtable, "line_lambda", i_row, NULL);
    double flux = cpl_table_get(aPixtable, "line_flux", i_row, NULL);
    double v = cpl_table_get(aPixtable, MUSE_PIXTABLE_LAMBDA, i_row, NULL)
             - lambda;
    cpl_array *val = cpl_array_wrap_double(&v, 1);
    muse_lsf_apply(cpl_imagelist_get(aLsfCube->img, i_slice - 1),
                   aLsfCube->wcs, val, lambda);
    cpl_array_unwrap(val);
    cpl_table_set(aPixtable, MUSE_PIXTABLE_DATA, i_row,
                  cpl_table_get(aPixtable, MUSE_PIXTABLE_DATA, i_row, NULL)
                  - v * flux);
  }
  return CPL_ERROR_NONE;
}

/**
   @brief Create a header with the QC parameters for the LSF.
   @param aLsfCube    Computed LSF cube
   @retval CPL_ERROR_NONE if ererything is OK,
   @cpl_ensure_code{aLsfCube, CPL_ERROR_NULL_INPUT}

   This function calculates for each slice the following parameters:

    - LSF SLICEj FWHM MEAN: Mean FWHM of the LSF slice j
    - LSF SLICEj FWHM STDEV: Standard deviation of the LSF in slice j
    - LSF SLICEj FWHM MIN: Minimum FWHM of the LSF in slice j
    - LSF SLICEj FWHM MAX: Maximum FWHM of the LSF in slice j

   They are added to the given header.
 */
static cpl_error_code
muse_lsf_qc(muse_lsf_cube *aLsfCube)
{
  cpl_ensure_code(aLsfCube, CPL_ERROR_NULL_INPUT);

  cpl_size i_slice, n_slices = cpl_imagelist_get_size(aLsfCube->img);
  for (i_slice = 0; i_slice < n_slices; i_slice++) {
    cpl_image *img = cpl_imagelist_get(aLsfCube->img, i_slice);
    cpl_size n_lsf = cpl_image_get_size_x(img);
    cpl_size n_lambda = cpl_image_get_size_y(img);
    cpl_array *fwhm = cpl_array_new(n_lambda, CPL_TYPE_DOUBLE);
    cpl_size i_lambda;
    for (i_lambda = 0; i_lambda < n_lambda; i_lambda++) {
      int res;
      double ref = cpl_image_get(img, n_lsf/2, i_lambda+1, &res);
      cpl_size i_min;
      for (i_min = n_lsf/2; i_min > 0; i_min--) {
        if (cpl_image_get(img, i_min, i_lambda+1, &res) < ref/2) {
          break;
        }
      }
      double f_min = (ref/2 - cpl_image_get(img, i_min, i_lambda+1, &res))
        / (cpl_image_get(img, i_min+1, i_lambda+1, &res)
           - cpl_image_get(img, i_min, i_lambda+1, &res));

      cpl_size i_max;
      for (i_max = n_lsf/2; i_max <= n_lsf; i_max++) {
        if (cpl_image_get(img, i_max, i_lambda+1, &res) < ref/2) {
          break;
        }
      }
      double f_max = (ref/2 - cpl_image_get(img, i_max, i_lambda+1, &res))
        / (cpl_image_get(img, i_max-1, i_lambda+1, &res)
           - cpl_image_get(img, i_max, i_lambda+1, &res));

      double f = (i_max - f_max - i_min - f_min) *  aLsfCube->wcs->cd11;
      cpl_array_set(fwhm, i_lambda, f);
    }
    char keyword[KEYWORD_LENGTH];
    snprintf(keyword, KEYWORD_LENGTH, "ESO QC LSF SLICE%i FWHM MEAN",
             (int)i_slice+1);
    cpl_propertylist_append_float(aLsfCube->header, keyword, cpl_array_get_mean(fwhm));
    snprintf(keyword, KEYWORD_LENGTH, "ESO QC LSF SLICE%i FWHM STDEV",
             (int)i_slice+1);
    cpl_propertylist_append_float(aLsfCube->header, keyword, cpl_array_get_stdev(fwhm));
    snprintf(keyword, KEYWORD_LENGTH, "ESO QC LSF SLICE%i FWHM MIN",
             (int)i_slice+1);
    cpl_propertylist_append_float(aLsfCube->header, keyword, cpl_array_get_min(fwhm));
    snprintf(keyword, KEYWORD_LENGTH, "ESO QC LSF SLICE%i FWHM MAX",
             (int)i_slice+1);
    cpl_propertylist_append_float(aLsfCube->header, keyword, cpl_array_get_max(fwhm));
    cpl_array_delete(fwhm);
  }

  return CPL_ERROR_NONE;
}

#ifdef USE_LSF_PARAMS
muse_lsf_params **
muse_lsf_params_compute(cpl_table *aLines,
                        muse_pixtable *aPixtable,
                        int max_iterations)
{
  muse_pixtable **slice_pixtable = muse_pixtable_extracted_get_slices(aPixtable);
  int n_slices = muse_pixtable_extracted_get_size(slice_pixtable);
  int i_slice;

  muse_lsf_params **lsfParams
    = cpl_calloc(n_slices+1, sizeof(muse_lsf_params *));
  #pragma omp parallel for default(none) num_threads(2)  /* as req. by Ralf */ \
          shared(aLines, n_slices, i_slice, lsfParams, slice_pixtable,         \
                 max_iterations)
  for (i_slice = 0; i_slice < n_slices; i_slice++) {
    uint32_t origin
      = (uint32_t)cpl_table_get_int(slice_pixtable[i_slice]->table,
                                    MUSE_PIXTABLE_ORIGIN, 0, NULL);
    unsigned short ifu = muse_pixtable_origin_get_ifu(origin);
    unsigned short slice = muse_pixtable_origin_get_slice(origin);

    cpl_errorstate prestate = cpl_errorstate_get();
    lsfParams[i_slice]
      = muse_lsf_params_fit(slice_pixtable[i_slice], aLines, max_iterations);
    if (!cpl_errorstate_is_equal(prestate)) {
      cpl_msg_error(__func__, "While processing slice %hu.%hu:", ifu, slice);
      cpl_errorstate_dump(prestate, CPL_FALSE, NULL);
      cpl_errorstate_set(prestate);
    }
  }
  muse_pixtable_extracted_delete(slice_pixtable);
  return lsfParams;
} /* muse_lsf_params_compute() */

/*----------------------------------------------------------------------------*/
/**
  @brief    Create a header with the QC parameters for the LSF parameters.
  @param    aMaster Computed sky parameters
  @return   Header with the QC parameters for the sky.
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
muse_lsf_params_qc(cpl_propertylist *aHeader, const muse_lsf_params **aSlicePars)
{
  cpl_ensure_code(aHeader != NULL, CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(aSlicePars != NULL, CPL_ERROR_NULL_INPUT);

  const muse_lsf_params **det;
  for (det = aSlicePars; *det != NULL; det++) {
    cpl_size n_lambda = 30;
    double lambda_step = (kMuseNominalLambdaMax - kMuseNominalLambdaMin)
      / (n_lambda - 1);
    cpl_array *fwhm = cpl_array_new(n_lambda, CPL_TYPE_DOUBLE);
    cpl_size i_lambda;
    for (i_lambda = 0; i_lambda < n_lambda; i_lambda++) {
      double lambda = kMuseNominalLambdaMin + i_lambda * lambda_step;
      double f = muse_lsf_fwhm_lambda(*det, lambda, 0.04, 150);
      cpl_array_set(fwhm, i_lambda, f);
    }
    char keyword[KEYWORD_LENGTH];
    snprintf(keyword, KEYWORD_LENGTH, "ESO QC LSF SLICE%i FWHM MEAN",
             (int)(*det)->slice);
    cpl_propertylist_append_float(aHeader, keyword, cpl_array_get_mean(fwhm));
    snprintf(keyword, KEYWORD_LENGTH, "ESO QC LSF SLICE%i FWHM STDEV",
             (int)(*det)->slice);
    cpl_propertylist_append_float(aHeader, keyword, cpl_array_get_stdev(fwhm));
    snprintf(keyword, KEYWORD_LENGTH, "ESO QC LSF SLICE%i FWHM MIN",
             (int)(*det)->slice);
    cpl_propertylist_append_float(aHeader, keyword, cpl_array_get_min(fwhm));
    snprintf(keyword, KEYWORD_LENGTH, "ESO QC LSF SLICE%i FWHM MAX",
             (int)(*det)->slice);
    cpl_propertylist_append_float(aHeader, keyword, cpl_array_get_max(fwhm));
    cpl_array_delete(fwhm);
  }

  return CPL_ERROR_NONE;
} /* muse_lsf_params_qc() */

static cpl_error_code
muse_lsf_params_save_table(cpl_propertylist *aHeader,
                     const muse_lsf_params **aSlicePars,
                     muse_processing *aProcessing, int aIFU)
{
  cpl_error_code r = CPL_ERROR_NONE;
  if (aSlicePars != NULL) {
    cpl_frame *frame
      = muse_processing_new_frame(aProcessing, aIFU, aHeader,
                                  "LSF_PROFILE", CPL_FRAME_TYPE_TABLE);
    if (frame != NULL) {
      const char *filename = cpl_frame_get_filename(frame);
      cpl_msg_info(__func__, "Saving LSF table as %s", filename);
      char *channel = cpl_sprintf("CHAN%02d", aIFU);
      cpl_propertylist_update_string(aHeader, "EXTNAME", channel);
      cpl_free(channel);
      r = cpl_propertylist_save(aHeader, filename, CPL_IO_CREATE);
      r = muse_lsf_params_save(aSlicePars, filename);
      if (r == CPL_ERROR_NONE) {
        cpl_frameset_insert(aProcessing->outframes, frame);
      } else {
        cpl_frame_delete(frame);
      }
    }
  }
  return r;
} /* muse_lsf_params_save_table() */
#endif // defined(USE_LSF_PARAMS)

/*----------------------------------------------------------------------------*/
/**
  @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_lsf_compute(muse_processing *aProcessing, muse_lsf_params_t *aParams)
{
  muse_basicproc_params *bpars = muse_basicproc_params_new(aProcessing->parameters,
                                                           "muse.muse_lsf");
  muse_imagelist *images = muse_basicproc_combine_images_lampwise(aProcessing,
                                                                  aParams->nifu,
                                                                  bpars, NULL);
  muse_basicproc_params_delete(bpars);
  cpl_ensure(images, cpl_error_get_code(), -1);

  cpl_table *arclines = muse_table_load(aProcessing, MUSE_TAG_LINE_CATALOG, 0);
  cpl_table *tracetable = muse_table_load(aProcessing, MUSE_TAG_TRACE_TABLE,
                                          aParams->nifu);
  cpl_table *wavecaltable = muse_table_load(aProcessing, MUSE_TAG_WAVECAL_TABLE,
                                            aParams->nifu);
  if (!arclines || !tracetable || !wavecaltable) {
    cpl_msg_error(__func__, "Calibration could not be loaded:%s%s%s",
                  !arclines ? " " : "",
                  !tracetable ? " "MUSE_TAG_TRACE_TABLE : "",
                  !wavecaltable ? " "MUSE_TAG_WAVECAL_TABLE : "");
    muse_imagelist_delete(images);
    cpl_table_delete(arclines);
    cpl_table_delete(tracetable);
    cpl_table_delete(wavecaltable);
    return -1;
  }

  /* keep the error state at this point, before doing the spectral processing */
  cpl_errorstate prestate = cpl_errorstate_get();

  if (aParams->method == MUSE_LSF_PARAM_METHOD_INTERPOLATE) {

    muse_pixtable *arcpixtable = muse_lsf_create_arcpixtable(
     images, tracetable, wavecaltable, arclines,
     aParams->line_quality,
     aParams->lsf_range + aParams->lsf_regression_window);

    cpl_table_delete(arclines);
    cpl_table_delete(tracetable);
    cpl_table_delete(wavecaltable);
    muse_imagelist_delete(images);
    if (arcpixtable == NULL) {
      cpl_msg_error(__func__, "Could not create pixel table of arc lines for "
                    "IFU %d", aParams->nifu);
      return -1;
    }

    muse_lsf_cube *lsfcube = muse_lsf_compute_slices(arcpixtable, aParams);
    if (lsfcube == NULL) {
      cpl_msg_error(__func__, "Could not create LSF cube for IFU %d",
                    aParams->nifu);
      if (aParams->save_subtracted) {
        muse_processing_save_table(aProcessing, aParams->nifu, arcpixtable, NULL,
                                   MUSE_TAG_PT_SUB, MUSE_TABLE_TYPE_PIXTABLE);
      }
      muse_pixtable_delete(arcpixtable);
      return -1;
    }

    /* add QC parameters to the primary header of the LSF cube and save it */
    muse_lsf_qc(lsfcube);
    muse_processing_save_cube(aProcessing, aParams->nifu, lsfcube,
                              MUSE_TAG_LSF_PROFILE, MUSE_CUBE_TYPE_LSF);
    if (aParams->save_subtracted) {
      cpl_table_duplicate_column(arcpixtable->table, "orig", arcpixtable->table,
                                 MUSE_PIXTABLE_DATA);
      muse_lsf_subtract_arcs(lsfcube, arcpixtable->table);
      muse_processing_save_table(aProcessing, aParams->nifu, arcpixtable, NULL,
                                 MUSE_TAG_PT_SUB, MUSE_TABLE_TYPE_PIXTABLE);
    }
    muse_pixtable_delete(arcpixtable);
    muse_lsf_cube_delete(lsfcube);

  }
#ifdef USE_LSF_PARAMS
  else if (aParams->method == MUSE_LSF_PARAM_METHOD_HERMIT) {
    muse_combinepar *cpars = muse_combinepar_new(aProcessing->parameters,
                                                 "muse.muse_lsf");
    muse_image *masterimage = muse_combine_images(cpars, images);
    muse_combinepar_delete(cpars);
    muse_imagelist_delete(images);

    muse_pixtable *arcpixtable = muse_pixtable_create(masterimage, tracetable,
                                                      wavecaltable, NULL);
    cpl_table_delete(tracetable);
    cpl_table_delete(wavecaltable);
    if (!arcpixtable) {
      cpl_msg_error(__func__, "ARC pixel table creation failed");
      cpl_table_delete(arclines);
      muse_image_delete(masterimage);
      return -1;
    }

    /* erase unused (flux == 0.) and non-isolated (quality < 3) lines */
    cpl_table_unselect_all(arclines);
    cpl_table_or_selected_int(arclines, MUSE_LINE_CATALOG_QUALITY,
                              CPL_LESS_THAN, aParams->line_quality);
    cpl_table_or_selected_float(arclines, "flux", CPL_NOT_GREATER_THAN, 0.);
    cpl_table_erase_selected(arclines);
    /* sort list by decreasing flux and cut it to the 40 brightest lines */
    cpl_propertylist *flux_sort = cpl_propertylist_new();
    cpl_propertylist_append_bool(flux_sort, "flux", CPL_TRUE);
    cpl_table_sort(arclines, flux_sort);
    cpl_propertylist_delete(flux_sort);
    cpl_size nrows = 40;
    if (nrows < cpl_table_get_nrow(arclines)) {
      cpl_table_erase_window(arclines, nrows, cpl_table_get_nrow(arclines));
    }

    if (aParams->save_subtracted) {
      cpl_table_duplicate_column(arcpixtable->table, "orig", arcpixtable->table,
                                 MUSE_PIXTABLE_DATA);
    }
    muse_lsf_params **lsf = muse_lsf_params_compute(arclines, arcpixtable, 40);

    cpl_propertylist *qc_header = cpl_propertylist_new();
    muse_lsf_params_qc(qc_header, (const muse_lsf_params **)lsf);
    muse_lsf_params_save_table(qc_header, (const muse_lsf_params **)lsf,
                               aProcessing, aParams->nifu);
    muse_lsf_params_delete_all(lsf);
    cpl_propertylist_delete(qc_header);

    if (aParams->save_subtracted) {
      muse_processing_save_table(aProcessing, aParams->nifu, arcpixtable, NULL,
                                 MUSE_TAG_PT_SUB, MUSE_TABLE_TYPE_PIXTABLE);
    }

    muse_pixtable_delete(arcpixtable);
    cpl_table_delete(arclines);
    muse_image_delete(masterimage);
  }
#endif // defined(USE_LSF_PARAMS)
  else {
    cpl_msg_error(__func__, "Unknown LSF method %s", aParams->method_s);
  }
  return (cpl_errorstate_is_equal(prestate))?0:-1;
}
