/* -*- 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

#define STORE_SLIT_WIDTH
#define STORE_BIN_WIDTH

/*----------------------------------------------------------------------------*
 *                             Includes                                       *
 *----------------------------------------------------------------------------*/
#include <math.h>
#include <string.h>
#include <fenv.h>

#include <cpl.h>

#include "muse_lsf.h"
#include "muse_resampling.h"
#include "muse_pfits.h"
#include "muse_quality.h"
#include "muse_instrument.h"
#include "muse_optimize.h"
#include "muse_tracing.h"
#include "muse_utils.h"

/*----------------------------------------------------------------------------*/
/**
   @defgroup muse_lsf       Line Spread Function related functions
 */
/*----------------------------------------------------------------------------*/

/**@{*/

/*----------------------------------------------------------------------------*
 *      Declaration of only internally used functions                         *
 *----------------------------------------------------------------------------*/
static cpl_error_code
muse_lsf_line_apply(const cpl_array *, cpl_array *,
                    const muse_lsf_params *, double, double);

/*----------------------------------------------------------------------------*/
/**
   @brief Create a new lsf_params structure.
   @param n_sensit Order of polynomial sensitivity parametrization.
   @param n_lsf_width Order of polynomial lsf width parametrization.
   @param n_hermit Order of polynomial parametrization of hermitean coefficients.
   @return Structure to hold the data.

   All polynomial parametrizations are meant as wavelength dependent.
 */
/*----------------------------------------------------------------------------*/
muse_lsf_params *
muse_lsf_params_new(cpl_size n_sensit, cpl_size n_lsf_width, cpl_size n_hermit)
{
  muse_lsf_params *res = cpl_calloc(1, sizeof(muse_lsf_params));
  res->refraction = 1.0;
  res->offset = 0.0;
  res->slit_width = kMuseSliceSlitWidthA;
  res->bin_width = kMuseSpectralSamplingA;
  res->lambda_ref = 7000;
  int i;
  if (n_hermit > 0) {
    for (i = 0; i < MAX_HERMIT_ORDER; i++) {
      res->hermit[i] = cpl_array_new(n_hermit, CPL_TYPE_DOUBLE);
      cpl_array_fill_window_double(res->hermit[i], 0, n_hermit, 0.0);
    }
  }
  res->lsf_width = cpl_array_new(n_lsf_width, CPL_TYPE_DOUBLE);
  if (n_lsf_width > 0) {
    cpl_array_fill_window_double(res->lsf_width, 0, n_lsf_width, 0.0);
    cpl_array_set_double(res->lsf_width, 0, 1.0);
  }
  res->sensitivity = cpl_array_new(n_sensit, CPL_TYPE_DOUBLE);
  if (n_sensit > 0) {
    cpl_array_fill_window_double(res->sensitivity, 0, n_sensit, 0.0);
    cpl_array_set_double(res->sensitivity, 0, 1.0);
  }
  return res;
}

/*----------------------------------------------------------------------------*/
/**
   @brief Count the number of entries in the array.
   @param aParams   LSF parameter array
   @return Number of entries in the array.
 */
/*----------------------------------------------------------------------------*/
cpl_size
muse_lsf_params_get_size(muse_lsf_params **aParams) {
  if (aParams == NULL) {
    return 0;
  }
  cpl_size i;
  for  (i = 0; *aParams != NULL; i++) {
    aParams++;
  }
  return i;
}

/*----------------------------------------------------------------------------*/
/**
   @brief Delete an allocated muse_lsf_params structure.
   @param aParams Structure to delete.
 */
/*----------------------------------------------------------------------------*/
void
muse_lsf_params_delete_one(muse_lsf_params *aParams) {
  if (aParams != NULL) {
    cpl_array_delete(aParams->sensitivity);
    int i;
    for (i = 0; i < MAX_HERMIT_ORDER; i++) {
      cpl_array_delete(aParams->hermit[i]);
    }
    cpl_array_delete(aParams->lsf_width);
    cpl_free(aParams);
  }
}

/*----------------------------------------------------------------------------*/
/**
   @brief Delete an allocated array of muse_lsf_params structure.
   @param aParams Structure to delete.
 */
/*----------------------------------------------------------------------------*/
void
muse_lsf_params_delete(muse_lsf_params **aParams) {
  if (aParams != NULL) {
    muse_lsf_params **det;
    for  (det = aParams; *det != NULL; det++) {
      muse_lsf_params_delete_one(*det);
    }
    cpl_free(aParams);
  }
}

/*----------------------------------------------------------------------------*/
/**
    @brief Definition of a lsf parameters table

    - <tt>ifu</tt>: IFU number
    - <tt>slice</tt>: slice number within the IFU
    - <tt>sensitivity</tt>sensitivity, relative to the reference
    - <tt>lsf_width</tt>: LSF gauss-hermitean width [Angstrom]
    - <tt>hermit3</tt>: 3rd order hermitean coefficient
    - <tt>hermit4</tt>: 4th order hermitean coefficient
    - <tt>hermit5</tt>: 5th order hermitean coefficient
    - <tt>hermit6</tt>: 6th order hermitean coefficient

*/
/*----------------------------------------------------------------------------*/
const muse_cpltable_def muse_lsfparams_def[] = {
  {"ifu", CPL_TYPE_INT, NULL, "%i", "IFU number", CPL_TRUE},
  {"slice", CPL_TYPE_INT, NULL, "%i", "slice number within the IFU", CPL_TRUE},
  {"sensitivity", CPL_TYPE_DOUBLE | CPL_TYPE_POINTER, NULL, "%e",
   "detector sensitivity, relative to the reference", CPL_TRUE},
  {"offset", CPL_TYPE_DOUBLE, NULL, "%e", "wavelength calibration offset", CPL_TRUE},
  {"refraction", CPL_TYPE_DOUBLE, NULL, "%e", "relative refraction index", CPL_TRUE},
#ifdef STORE_SLIT_WIDTH
  {"slit_width", CPL_TYPE_DOUBLE, "Angstrom", "%e", "slit width", CPL_TRUE},
#endif
#ifdef STORE_BIN_WIDTH
  {"bin_width", CPL_TYPE_DOUBLE, "Angstrom", "%e", "bin width", CPL_TRUE},
#endif
  {"lsf_width", CPL_TYPE_DOUBLE | CPL_TYPE_POINTER, "Angstrom", "%e",
   " LSF gauss-hermitean width", CPL_TRUE},
  {"hermit3", CPL_TYPE_DOUBLE | CPL_TYPE_POINTER, NULL, "%e",
   "3rd order hermitean coefficient", CPL_TRUE},
  {"hermit4", CPL_TYPE_DOUBLE | CPL_TYPE_POINTER, NULL, "%e",
   "4th order hermitean coefficient", CPL_TRUE},
  {"hermit5", CPL_TYPE_DOUBLE | CPL_TYPE_POINTER, NULL, "%e",
   "5th order hermitean coefficient", CPL_TRUE},
  {"hermit6", CPL_TYPE_DOUBLE | CPL_TYPE_POINTER, NULL, "%e",
   "6th order hermitean coefficient", CPL_TRUE},
  { NULL, 0, NULL, NULL, NULL, CPL_FALSE }
};

/*----------------------------------------------------------------------------*/
/**
   @brief Save slice LSF parameters to the extension "slice" on disk.
   @param aLsf NULL-terminated array of LSF parameters.
   @param aFile File name (file must exist, is written as extension)
   @retval CPL_ERROR_NONE if everything went OK
   @cpl_ensure_code{aLsf != NULL, CPL_ERROR_NULL_INPUT}
   @cpl_ensure_code{*aLsf != NULL, CPL_ERROR_DATA_NOT_FOUND}
   @cpl_ensure_code{aFile != NULL, CPL_ERROR_NULL_INPUT}

   The slice parameters are converted to a table structure:

   @copydetails muse_lsfparams_def
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_lsf_params_save(const muse_lsf_params **aLsf, const char *aFile) {
  cpl_ensure_code(aLsf != NULL, CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(*aLsf != NULL, CPL_ERROR_DATA_NOT_FOUND);
  cpl_ensure_code(aFile != NULL, CPL_ERROR_NULL_INPUT);

  cpl_size nrows = 0;
  const muse_lsf_params **det;
  cpl_size sensitivity_order = 1;
  cpl_size lsf_order = 1;
  cpl_size hermit_order[MAX_HERMIT_ORDER];
  int i;
  for (i = 0; i < MAX_HERMIT_ORDER; i++) {
    hermit_order[i] = 1;
  }
  for (det = aLsf; *det != NULL; det++, nrows++) {
    sensitivity_order = fmax(sensitivity_order,
                             cpl_array_get_size((*det)->sensitivity));
    lsf_order = fmax(lsf_order, cpl_array_get_size((*det)->lsf_width));
    for (i = 0; i < MAX_HERMIT_ORDER; i++) {
      hermit_order[i] = fmax(hermit_order[i],
                             cpl_array_get_size((*det)->hermit[i]));
    }
  }

  cpl_table *slice_param = cpl_table_new(nrows);
  cpl_table_new_column(slice_param, "ifu", CPL_TYPE_INT);
  cpl_table_new_column(slice_param, "slice", CPL_TYPE_INT);
  cpl_table_new_column_array(slice_param, "sensitivity",
                             cpl_array_get_type(aLsf[0]->sensitivity),
                             sensitivity_order);
  cpl_table_new_column(slice_param, "offset", CPL_TYPE_DOUBLE);
  cpl_table_new_column(slice_param, "refraction", CPL_TYPE_DOUBLE);
#ifdef STORE_SLIT_WIDTH
  cpl_table_new_column(slice_param, "slit_width", CPL_TYPE_DOUBLE);
#endif
#ifdef STORE_BIN_WIDTH
  cpl_table_new_column(slice_param, "bin_width", CPL_TYPE_DOUBLE);
#endif
  cpl_table_new_column_array(slice_param, "lsf_width",
                             cpl_array_get_type(aLsf[0]->lsf_width),
                             lsf_order);
  cpl_table_new_column_array(slice_param, "hermit3",
                             cpl_array_get_type(aLsf[0]->hermit[0]),
                             hermit_order[0]);
  cpl_table_new_column_array(slice_param, "hermit4",
                             cpl_array_get_type(aLsf[0]->hermit[1]),
                             hermit_order[1]);
  cpl_table_new_column_array(slice_param, "hermit5",
                             cpl_array_get_type(aLsf[0]->hermit[2]),
                             hermit_order[2]);
  cpl_table_new_column_array(slice_param, "hermit6",
                             cpl_array_get_type(aLsf[0]->hermit[3]),
                             hermit_order[3]);

  cpl_size iRow = 0;
  for (det = aLsf; *det != NULL; det++, iRow++) {
    cpl_table_set(slice_param, "ifu", iRow, (*det)->ifu);
    cpl_table_set(slice_param, "slice", iRow, (*det)->slice);
    cpl_table_set_array(slice_param, "sensitivity", iRow, (*det)->sensitivity);
    cpl_table_set(slice_param, "offset", iRow, (*det)->offset);
    cpl_table_set(slice_param, "refraction", iRow, (*det)->refraction);
#ifdef STORE_SLIT_WIDTH
    cpl_table_set(slice_param, "slit_width", iRow, (*det)->slit_width);
#endif
#ifdef STORE_BIN_WIDTH
    cpl_table_set(slice_param, "bin_width", iRow, (*det)->bin_width);
#endif
    cpl_table_set_array(slice_param, "lsf_width", iRow, (*det)->lsf_width);
    cpl_table_set_array(slice_param, "hermit3", iRow, (*det)->hermit[0]);
    cpl_table_set_array(slice_param, "hermit4", iRow, (*det)->hermit[1]);
    cpl_table_set_array(slice_param, "hermit5", iRow, (*det)->hermit[2]);
    cpl_table_set_array(slice_param, "hermit6", iRow, (*det)->hermit[3]);
  }

  int r = muse_cpltable_append_file(slice_param, aFile, "SLICE_PARAM",
                                    muse_lsfparams_def);
  cpl_table_delete(slice_param);
  return r;
}

/*----------------------------------------------------------------------------*/
/**
   @brief Load slice LSF parameters from the extension "SLICE_PARAM".
   @param aFile File name (file must exist, is written as extension)
   @param aParams NULL-terminated array of LSF parameters to append to,
          or NULL.
   @param aIFU the IFU/channel number, or 0 for all IFUs/channels
   @return NULL-terminated array of LSF parameters.

   The extension "SLICE_PARAM" (or "CHANnn.SLICE_PARAM") is expected to be a
   table with the @ref muse_lsfparams_def "slice dependent LSF parameters".
 */
/*----------------------------------------------------------------------------*/
muse_lsf_params **
muse_lsf_params_load(const char *aFile, muse_lsf_params **aParams, int aIFU)
{
  cpl_table *lsfTable = muse_cpltable_load(aFile, "SLICE_PARAM",
                                           muse_lsfparams_def);
  if (!lsfTable) {
    /* try to load from channel extensions */
    char *extname = cpl_sprintf("CHAN%02d.SLICE_PARAM", aIFU);
    lsfTable = muse_cpltable_load(aFile, extname, muse_lsfparams_def);
    cpl_free(extname);
    if (!lsfTable) {
      /* now display both the older and the new error messages */
      cpl_error_set_message(__func__, cpl_error_get_code(), "Loading LSF data from "
                            "\"%s[SLICE_PARAMS]\" and \"%s[CHAH%02d.SLICE_PARAMS]\" "
                            "failed", aFile, aFile, aIFU);
      return aParams;
    } /* if 2nd load failure */
  } /* if 1st load failure */

  cpl_size n_rows = cpl_table_get_nrow(lsfTable);
  cpl_size n_rows_old = muse_lsf_params_get_size(aParams);
  muse_lsf_params **lsfParams
    = cpl_realloc(aParams, (n_rows + n_rows_old + 1) * sizeof(muse_lsf_params *));
  lsfParams[n_rows + n_rows_old] = NULL;
  cpl_size i_row_new = n_rows_old;
  cpl_size i_row;
  for (i_row = 0; i_row < n_rows; i_row++) {
    int ifu = cpl_table_get(lsfTable, "ifu", i_row, NULL);
    lsfParams[i_row + n_rows_old] = NULL;
    if ((aIFU <= 0) || (ifu == aIFU)) {
      muse_lsf_params *det = muse_lsf_params_new(0,0,0);
      lsfParams[i_row_new] = det;
      i_row_new++;
      det->ifu = ifu;
      det->slice = cpl_table_get(lsfTable, "slice", i_row, NULL);
      cpl_array_delete(det->sensitivity);
      det->sensitivity
        = muse_cpltable_get_array_copy(lsfTable, "sensitivity",i_row);
      det->offset = cpl_table_get(lsfTable, "offset", i_row, NULL);
      det->refraction = cpl_table_get(lsfTable, "refraction", i_row, NULL);
#ifdef STORE_SLIT_WIDTH
      det->slit_width = cpl_table_get(lsfTable, "slit_width", i_row, NULL);
#endif
#ifdef STORE_BIN_WIDTH
      det->bin_width = cpl_table_get(lsfTable, "bin_width", i_row, NULL);
#endif
      cpl_array_delete(det->lsf_width);
      det->lsf_width
        = muse_cpltable_get_array_copy(lsfTable, "lsf_width", i_row);
      cpl_array_delete(det->hermit[0]);
      det->hermit[0]
        = muse_cpltable_get_array_copy(lsfTable, "hermit3", i_row);
      cpl_array_delete(det->hermit[1]);
      det->hermit[1]
        = muse_cpltable_get_array_copy(lsfTable, "hermit4", i_row);
      cpl_array_delete(det->hermit[2]);
      det->hermit[2]
        = muse_cpltable_get_array_copy(lsfTable, "hermit5", i_row);
      cpl_array_delete(det->hermit[3]);
      det->hermit[3]
        = muse_cpltable_get_array_copy(lsfTable, "hermit6", i_row);
    }
  }
  cpl_table_delete(lsfTable);

  return lsfParams;
}

/*----------------------------------------------------------------------------*/
/**
  @brief Load slice LSF parameters
  @param    aProcessing   the processing structure
  @param    aIFU          the IFU/channel number, or 0 for all IFUs/channels
  @return   a pointer to the LSF params or NULL on error

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         invalid processing pointer}
  @error{propagate CPL error code\, return NULL,
         LST_TABLE frame not found in input frameset of the processing structure}

   The input files must be tables with the
   @ref muse_lsfparams_def "slice dependent LSF parameters".
 */
/*----------------------------------------------------------------------------*/
muse_lsf_params **
muse_processing_lsf_params_load(muse_processing *aProcessing, int aIFU)
{
  cpl_ensure(aProcessing, CPL_ERROR_NULL_INPUT, NULL);
  cpl_frameset *frames = muse_frameset_find(aProcessing->inframes,
                                            MUSE_TAG_LSF_PROFILE, aIFU,
                                            CPL_FALSE);
  if (frames == NULL) {
    return NULL;
  }
  /* try standard format first: one file per IFU */
  cpl_errorstate state = cpl_errorstate_get();
  cpl_size iframe, nframes = cpl_frameset_get_size(frames);
  muse_lsf_params **lsfParams = NULL;
  for (iframe = 0; iframe < nframes; iframe++) {
    cpl_frame *frame = cpl_frameset_get_position(frames, iframe);
    lsfParams = muse_lsf_params_load(cpl_frame_get_filename(frame),
                                     lsfParams, aIFU);
    if (lsfParams) {
      cpl_msg_info(__func__, "Loaded slice LSF params from \"%s\"",
                   cpl_frame_get_filename(frame));
      muse_processing_append_used(aProcessing, frame, CPL_FRAME_GROUP_CALIB, 1);
    }
  } /* for iframe (all frames) */
  char *errmsg = NULL;
  if (!cpl_errorstate_is_equal(state)) {
    errmsg = cpl_strdup(cpl_error_get_message());
  }
  cpl_errorstate_set(state); /* hide error(s) */

  /* extra loop to support the merged format */
  if (!lsfParams && aIFU == 0 && nframes == 1) {
    cpl_msg_debug(__func__, "No LSF parameters loaded yet, trying merged table "
                  "format.");

    cpl_frame *frame = cpl_frameset_get_position(frames, 0);
    const char *fname = cpl_frame_get_filename(frame);
    state = cpl_errorstate_get();
    unsigned char ifu;
    for (ifu = 1; ifu <= kMuseNumIFUs; ifu++) {
      lsfParams = muse_lsf_params_load(fname, lsfParams, ifu);
    } /* for ifu */
    cpl_errorstate_set(state); /* ignore errors */
    if (lsfParams) {
      cpl_msg_info(__func__, "Loaded (merged) slice LSF params from \"%s\"", fname);
      muse_processing_append_used(aProcessing, frame, CPL_FRAME_GROUP_CALIB, 1);
    }
  } /* if */
  cpl_frameset_delete(frames);

  /* now possible output error message */
  if (!lsfParams) {
    cpl_msg_debug(__func__, "Loading %ss from input frameset did not succeed: "
                  "%s", MUSE_TAG_LSF_PROFILE, errmsg);
  } /* if still no lsfParams */
  cpl_free(errmsg);
  return lsfParams;
} /* muse_processing_lsf_params_load() */

/*----------------------------------------------------------------------------*/
/**
   @brief Get the slice LSF parameters for one slice.
   @return Pointer to parameter structure.
 */
/*----------------------------------------------------------------------------*/
muse_lsf_params *
muse_lsf_params_get(muse_lsf_params **aParams, int aIFU, int aSlice) {
  if (aParams == NULL) {
    return NULL;
  }
  int i_det;
  for (i_det = 0; aParams[i_det] != NULL; i_det++) {
    if (aParams[i_det]->ifu == aIFU && aParams[i_det]->slice == aSlice) {
      return aParams[i_det];
    }
  }
  return NULL;
}

/*----------------------------------------------------------------------------*/
/**
  @brief Helper function "G" for integrated damped gauss-hermitean function
  @param aX X values array to apply
  @param aCoeffs pointer to five values for the damping polynomial
  @return The return value array.
  @cpl_ensure{aX != NULL, CPL_ERROR_NULL_INPUT, 0.0}
  @cpl_ensure{aCoeffs != NULL, CPL_ERROR_NULL_INPUT, 0.0}

  @pseudocode
R(x) = aCoeffs[0] * x**4 + aCoeffs[1] * x**3
     + aCoeffs[2] * x**2 + aCoeffs[3] * x + aCoeffs[4]

G(x) =  exp(-0.5 * x**2)
      + R(x) * exp(-x**2) / 60
      + x * sqrt(Pi/2) * erf(x * sqrt(0.5))@endpseudocode
*/
/*----------------------------------------------------------------------------*/
static cpl_array *
muse_lsf_G(cpl_array *aX, cpl_array *aCoeffs) {
  cpl_ensure(aX != NULL, CPL_ERROR_NULL_INPUT, NULL);
  cpl_ensure(aCoeffs != NULL, CPL_ERROR_NULL_INPUT, NULL);

  cpl_array *y = cpl_array_duplicate(aX);
  cpl_array_multiply(y, y);
  cpl_array_multiply_scalar(y, -1);  // y = - x**2

  cpl_array *y2 = cpl_array_duplicate(y);
  muse_cplarray_exp(y2);
  cpl_array_multiply_scalar(y2, 1.0/60); // y2 = exp(-x**2)/60

  cpl_array_multiply_scalar(y, 0.5);
  muse_cplarray_exp(y); // y = exp(-0.5 * x**2)

  cpl_array *R = cpl_array_duplicate(aX);
  muse_cplarray_poly1d(R, aCoeffs);
  cpl_array_multiply(y2, R); // y2 = R * exp(-x**2)/60
  cpl_array_delete(R);
  cpl_array_add(y, y2);

  cpl_array_copy_data_double(y2, cpl_array_get_data_double(aX));
  cpl_array_multiply_scalar(y2, sqrt(0.5));
  muse_cplarray_erf(y2);
  cpl_array_multiply_scalar(y2, sqrt(CPL_MATH_PI / 2));
  cpl_array_multiply(y2, aX); // y3 = x * sqrt(Pi/2) * erf(x/sqrt(2))
  cpl_array_add(y, y2);
  cpl_array_delete(y2);

  return y;
}

/*----------------------------------------------------------------------------*/
/**
  @brief Apply the MUSE LSF function to a single line.
  @param aLambda Wavelength bins [Angstrom]
  @param aSpectrum Spectrum to add the line
  @param aLsf LSF parameters
  @param aLineLambda Line wavelength
  @param aLineFlux   Line flux
  @retval CPL_ERROR_NONE Everything went OK
  @cpl_ensure_code{aSpectrum != NULL, CPL_ERROR_NULL_INPUT}
  @cpl_ensure_code{aParam != NULL, CPL_ERROR_NULL_INPUT}
  @cpl_ensure_code{cpl_table_has_column(aSpectrum\, "lines"),
                   CPL_ERROR_DATA_NOT_FOUND}

  @pseudocode
x = (aLambda - aLineLambda) / aLsf.width
slit_width = aLsf.slit_width / aLsf.width
bin_width  = aLsf.bin_width  / aLsf.width

y = G(x + slit_width/2 + bin_width/2) - G(x - slit_width/2 + bin_width/2)
  - G(x + slit_width/2 - bin_width/2) + G(x - slit_width/2 - bin_width/2)

spectrum += y * aLineFlux@endpseudocode

 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
muse_lsf_line_apply(const cpl_array *aLambda, cpl_array *aSpectrum,
                    const muse_lsf_params *aLsf,
                    double aLineLambda, double aLineFlux) {
  cpl_ensure_code(aSpectrum != NULL, CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(aLambda != NULL, CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(aLsf != NULL, CPL_ERROR_NULL_INPUT);

  aLineLambda *= aLsf->refraction;
  aLineLambda += aLsf->offset;

  double slit_width = aLsf->slit_width;
  double bin_width = aLsf->bin_width;
  double width = muse_cplarray_poly1d_double(aLineLambda - aLsf->lambda_ref,
                                             aLsf->lsf_width);
  double h3 = muse_cplarray_poly1d_double(aLineLambda - aLsf->lambda_ref,
                                          aLsf->hermit[0]);
  double h4 = muse_cplarray_poly1d_double(aLineLambda - aLsf->lambda_ref,
                                          aLsf->hermit[1]);
  double h5 = muse_cplarray_poly1d_double(aLineLambda - aLsf->lambda_ref,
                                          aLsf->hermit[2]);
  double h6 = muse_cplarray_poly1d_double(aLineLambda - aLsf->lambda_ref,
                                          aLsf->hermit[3]);

  cpl_array *coeff = cpl_array_new(5, CPL_TYPE_DOUBLE);
  cpl_array_set(coeff, 4, 2 * sqrt(5) * h6);
  cpl_array_set(coeff, 3, 2 * sqrt(15) * h5);
  cpl_array_set(coeff, 2, 5 * sqrt(6) * h4 - 6 * sqrt(5) * h6);
  cpl_array_set(coeff, 1, 10 * sqrt(3) * h3 - 3 * sqrt(15) * h5);
  cpl_array_set(coeff, 0, -2.5 * sqrt(6) * h4 + 1.5 * sqrt(5) * h6);

  int imin = muse_cplarray_find_sorted(aLambda,
                                       aLineLambda - (5 * width +
                                                      0.6*(bin_width + slit_width)));
  int imax = muse_cplarray_find_sorted(aLambda,
                                       aLineLambda + (5 * width +
                                                      0.6*(bin_width + slit_width)));
  if (imax < imin) {
    cpl_array_delete(coeff);
    return CPL_ERROR_NONE;
  }

  cpl_array *l0 = cpl_array_extract(aLambda, imin, imax-imin+1);

  cpl_array_subtract_scalar(l0, aLineLambda);
  cpl_array_divide_scalar(l0, width);
  bin_width /= 2 * width;
  slit_width /= 2 * width;

  cpl_array *x = cpl_array_duplicate(l0);
  cpl_array *y;
  cpl_array *y1;
  cpl_array_add_scalar(x, slit_width + bin_width);
  y = muse_lsf_G(x, coeff);

  cpl_array_copy_data_double(x, cpl_array_get_data_double(l0));
  cpl_array_add_scalar(x, slit_width - bin_width);
  y1 = muse_lsf_G(x, coeff);
  cpl_array_subtract(y, y1);
  cpl_array_delete(y1);

  cpl_array_copy_data_double(x, cpl_array_get_data_double(l0));
  cpl_array_add_scalar(x, -slit_width + bin_width);
  y1 = muse_lsf_G(x, coeff);
  cpl_array_subtract(y, y1);
  cpl_array_delete(y1);

  cpl_array_copy_data_double(x, cpl_array_get_data_double(l0));
  cpl_array_add_scalar(x, -slit_width - bin_width);
  y1 = muse_lsf_G(x, coeff);
  cpl_array_delete(x);
  cpl_array_add(y, y1);
  cpl_array_delete(y1);

//  cpl_array_divide_scalar(y, sqrt(CPL_MATH_PI * width / 2));
  cpl_array_multiply_scalar(y, width);
  cpl_array_multiply_scalar(y, aLineFlux);

  muse_cplarray_add_window(aSpectrum, imin, y);
  cpl_array_delete(y);
  cpl_array_delete(l0);
  cpl_array_delete(coeff);

  return CPL_ERROR_NONE;
}

/*----------------------------------------------------------------------------*/
/**
   @brief Set the lines spectrum from the lines table and the detector LSF.
   @param aLambda Wavelength bins
   @param aLinesLambda Array with lines wavelengths
   @param aLinesFlux Array with lines fluxes
   @param aLsf LSF parameters
   @return Spectrum with flux from lines.
   @cpl_ensure{aSpectrum != NULL, CPL_ERROR_NULL_INPUT, NULL}
   @cpl_ensure{aLines != NULL, CPL_ERROR_NULL_INPUT, NULL}
   @cpl_ensure{aLsf != NULL, CPL_ERROR_NULL_INPUT, NULL}

   @pseudocode
spectrum = 0
for line in aLines:
    spectrum += muse_lsf_line_apply(aLsf, line.lambda, line.flux)
return spectrum@endpseudocode

 */
/*----------------------------------------------------------------------------*/
cpl_array *
muse_lsf_spectrum_get_lines(const cpl_array *aLambda,
                            const cpl_array *aLinesLambda,
                            const cpl_array *aLinesFlux,
                            const muse_lsf_params *aLsf) {
  cpl_ensure(aLambda != NULL, CPL_ERROR_NULL_INPUT, NULL);
  cpl_ensure(aLinesLambda != NULL, CPL_ERROR_NULL_INPUT, NULL);
  cpl_ensure(aLinesFlux != NULL, CPL_ERROR_NULL_INPUT, NULL);
  cpl_ensure(aLsf != NULL, CPL_ERROR_NULL_INPUT, NULL);

  int nrows = cpl_array_get_size(aLambda);
  cpl_array *spectrum = cpl_array_new(nrows, CPL_TYPE_DOUBLE);
  cpl_array_fill_window_double(spectrum, 0, nrows, 0.0);
  int i;
  int linescount = cpl_array_get_size(aLinesLambda);
  int errold = errno;
  feclearexcept(FE_UNDERFLOW);
  for (i = 0; i < linescount; i++) {
    double lambda = cpl_array_get(aLinesLambda, i, NULL);
    double flux = cpl_array_get(aLinesFlux, i, NULL);
    muse_lsf_line_apply(aLambda, spectrum, aLsf, lambda, flux);
  }
  if (fetestexcept(FE_UNDERFLOW)) {
    errno = errold;
    feclearexcept(FE_UNDERFLOW);
  }
  return spectrum;
}

/*----------------------------------------------------------------------------*/
/**
  @brief   Measure the FWHM of an LSF at a given wavelength.
  @param   aDP          the LSF parameters (for one IFU and slice)
  @param   aLambda      wavelength at which to evaluate the LSF
  @param   aFlux        flux of the line to distribute
  @param   aSampling    sampling of the spectrum in Angstrom
  @param   aLength      length of spectrum in pixels
  @param   aOutstream   file pointer for verbose output (can be NULL)
  @return  The FWHM of the LSF at this wavelength or 0. on error.

  This function creates a spectrum of given aLength with given resolution
  (aSampling), fills it with a line with given flux (aFlux) at the given
  wavelength aLambda. This spectrum is then used to measures peak intensity,
  searches for the position where the intensity reaches half the peak, to
  derive the FWHM from this.
  If aOutstream is not NULL, debug output about the parameters and measurement
  values is written to this file pointer.

  @error{set CPL_ERROR_NULL_INPUT\, return 0., aDP is NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return 0.,
         aDP contains lsf_width of zero or one}
  @error{set CPL_ERROR_ILLEGAL_OUTPUT\, return 0.,
         FWHM is outside the test spectrum for the given parameters}
 */
/*----------------------------------------------------------------------------*/
double
muse_lsf_fwhm_lambda(const muse_lsf_params *aDP, double aLambda,
                     double aFlux, double aSampling, unsigned int aLength,
                     FILE *aOutstream)
{
  cpl_ensure(aDP, CPL_ERROR_NULL_INPUT, 0.);
  /* if the lsf_width was not fitted correctly this value would be useless */
  cpl_ensure(cpl_array_get(aDP->lsf_width, 0, NULL) != 1 &&
             cpl_array_get(aDP->lsf_width, 0, NULL) != 0,
             CPL_ERROR_ILLEGAL_INPUT, 0.);

  cpl_array *llambda = cpl_array_new(1, CPL_TYPE_DOUBLE),
            *lflux = cpl_array_new(1, CPL_TYPE_DOUBLE);
  cpl_array_set_double(llambda, 0, aLambda);
  cpl_array_set_double(lflux, 0, aFlux);

  cpl_array *lambda = cpl_array_new(aLength, CPL_TYPE_DOUBLE);
  cpl_size i;
  for (i = 0; i < aLength; i++) {
    cpl_array_set_double(lambda, i, (i + 1. - aLength/2) * aSampling + aLambda);
  } /* for i */

  cpl_array *spec = muse_lsf_spectrum_get_lines(lambda, llambda, lflux, aDP);
  cpl_size imax;
  cpl_array_get_maxpos(spec, &imax);
  double max = cpl_array_get_max(spec),
         vl = 0., vr = 0.; /* exact values at half maximum */
  i = imax;
  while (--i >= 0 &&
         (vl = cpl_array_get_double(spec, i, NULL)) > max/2.) ;
  cpl_size il = i; /* first point with value below half max */
  i = imax;
  while (++i < aLength &&
         (vr = cpl_array_get_double(spec, i, NULL)) > max/2.) ;
  cpl_size ir = i;
  /* compute the FWHM that we really want; do not use (ir - il + 1) *
   * because on average we stepped two half pixels too far...       */
  double fwhm = (ir - il) * aSampling;
  if (aOutstream) {
    fprintf(aOutstream, "  %.1f:  %02d.%02d\t%f %f %f\t%f +/- %f (%f) %f..%f "
            "(%"CPL_SIZE_FORMAT")\t%f (%"CPL_SIZE_FORMAT") %f (%"CPL_SIZE_FORMAT
            ") ==> %f\n", aLambda, aDP->ifu, aDP->slice, aDP->lambda_ref,
            aDP->slit_width, aDP->bin_width, cpl_array_get_mean(spec),
            cpl_array_get_stdev(spec), cpl_array_get_median(spec),
            cpl_array_get_min(spec), max, imax, vl, il, vr, ir, fwhm);
  }
#if 0
  cpl_array_dump(spec, 0, aLength, stdout);
#endif
#if 0
  cpl_vector *vspec = cpl_vector_wrap(aLength, cpl_array_get_data_double(spec));
  cpl_vector_save(vspec, "vspec.fits", CPL_TYPE_DOUBLE, NULL, CPL_IO_CREATE);
  cpl_vector_unwrap(vspec);
#endif
  cpl_array_delete(spec);
  cpl_array_delete(lambda);
  cpl_array_delete(llambda);
  cpl_array_delete(lflux);
  cpl_ensure(il > 0 && ir < aLength, CPL_ERROR_ILLEGAL_OUTPUT, 0.);
  return fwhm;
} /* muse_lsf_fwhm_lambda() */

/**@}*/
