/* -*- 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) 2015 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 "muse_dfs.h"
#include "muse_pfits.h"
#include "muse_pixtable.h"
#include "muse_cplwrappers.h"
#include "muse_wcs.h"
#include "muse_flux.h"
#include "muse_utils.h"
#include "muse_idp.h"

/*----------------------------------------------------------------------------*
 *                              Defines                                       *
 *----------------------------------------------------------------------------*/

#define KEY_ASSON                    "ASSON"
#define KEY_ASSON_COMMENT            "Associated file name"
#define KEY_DEC                      "DEC"
#define KEY_DEC_COMMENT              "[deg] Image center (J2000)"
#define KEY_EXPTIME                  "EXPTIME"
#define KEY_EXPTIME_COMMENT          "[s] Total integration time per pixel"
#define KEY_FLUXCAL                  "FLUXCAL"
#define KEY_FLUXCAL_COMMENT          "Type of flux calibration (ABSOLUTE or UNCALIBRATED)"
#define KEY_FLUXCAL_VALUE_FALSE      "UNCALIBRATED"
#define KEY_FLUXCAL_VALUE_TRUE       "ABSOLUTE"
#define KEY_MJDOBS                   "MJD-OBS"
#define KEY_MJDOBS_COMMENT           "[d] Start of observations (days)"
#define KEY_MJDEND                   "MJD-END"
#define KEY_MJDEND_COMMENT           "[d] End of observations (days)"
#define KEY_OBID                     "OBID"
#define KEY_OBID_COMMENT             "Observation block ID"
#define KEY_OBSTECH                  "OBSTECH"
#define KEY_OBSTECH_COMMENT          "Technique for observation"
#define KEY_PROCSOFT                 "PROCSOFT"
#define KEY_PROCSOFT_COMMENT         "ESO pipeline version"
#define KEY_PRODCATG                 "PRODCATG"
#define KEY_PRODCATG_COMMENT         "Data product category"
#define KEY_PRODCATG_VALUE_IFS_CUBE  "SCIENCE.CUBE.IFS"
#define KEY_PRODCATG_VALUE_FOV_IMAGE "ANCILLARY.IMAGE"
#define KEY_PROG_ID                  "PROG_ID"
#define KEY_PROG_ID_COMMENT          "ESO programme identification"
#define KEY_PROG_ID_VALUE_MULTIPLE   "MULTI"
#define KEY_PROGID                   "PROGID"
#define KEY_PROGID_COMMENT           KEY_PROG_ID_COMMENT
#define KEY_PROV                     "PROV"
#define KEY_PROV_COMMENT             "Originating raw science file"
#define KEY_RA                       "RA"
#define KEY_RA_COMMENT               "[deg] Image center (J2000)"
#define KEY_REFERENC                 "REFERENC"
#define KEY_REFERENC_COMMENT         "Reference publication"
#define KEY_TEXPTIME                 "TEXPTIME"
#define KEY_TEXPTIME_COMMENT         "[s] Total integration time of all exposures"
#define KEY_WAVELMIN                 "WAVELMIN"
#define KEY_WAVELMIN_COMMENT         "[nm] Minimum wavelength"
#define KEY_WAVELMAX                 "WAVELMAX"
#define KEY_WAVELMAX_COMMENT         "[nm] Maximum wavelength"
#define KEY_SKY_RES                  "SKY_RES"
#define KEY_SKY_RES_COMMENT          "[arcsec] FWHM effective spatial resolution (measured)"
#define KEY_SKY_RERR                 "SKY_RERR"
#define KEY_SKY_RERR_COMMENT         "[arcsec] Error of SKY_RES (estimated)"
#define KEY_SPEC_RES                 "SPEC_RES"
#define KEY_SPEC_RES_COMMENT         "Spectral resolving power at central wavelength"
#define KEY_SPEC_ERR                 "CRDER3"
#define KEY_SPEC_ERR_COMMENT         "[Angstrom] Random error in spectral coordinate"
#define KEY_PIXNOISE                 "PIXNOISE"
#define KEY_PIXNOISE_COMMENT         "[erg/s/cm**2/Angstrom] pixel-to-pixel noise"
#define KEY_ABMAGLIM                 "ABMAGLIM"
#define KEY_ABMAGLIM_COMMENT         "5-sigma magnitude limit for point sources"
#define KEY_NCOMBINE                 "NCOMBINE"
#define KEY_NCOMBINE_COMMENT         "No. of combined raw science data files"

/* A regular expression matching SDP IFS standard keywords to be cleared. */
#define CLEAN_KEYS_REGEXP \
  "^(" KEY_MJDEND "|" \
  KEY_PROCSOFT "|" \
  KEY_PRODCATG "|" \
  KEY_PROG_ID "|" \
  KEY_PROGID "[0-9]+|" \
  KEY_OBID "[0-9]+|" \
  KEY_OBSTECH "|" \
  KEY_FLUXCAL "|" \
  KEY_TEXPTIME "|" \
  KEY_WAVELMIN "|" \
  KEY_WAVELMAX "|" \
  KEY_SKY_RES "|" \
  KEY_SKY_RERR "|" \
  KEY_SPEC_RES "|" \
  KEY_PIXNOISE "|" \
  KEY_ABMAGLIM "|" \
  KEY_REFERENC "|" \
  KEY_NCOMBINE "|" \
  KEY_PROV "[0-9]+|" \
  KEY_ASSON "[0-9]+" ")$"

/*----------------------------------------------------------------------------*/
/**
 * @defgroup muse_idp          ESO science data product format support
 */
/*----------------------------------------------------------------------------*/

/**@{*/

/* Lookup table for spectral resolution vs. wavelength */

typedef struct {
    /* Wavelength in Angstrom at which the spectral resolution was measured. */
    double lambda;
    /* Measured spectral resolution. */
    double R;
} muse_specres_lut_entry;

/* The last entry in the data tables must have a zero wavelength entry! */

static const muse_specres_lut_entry
muse_specres_data_wfm[] =
{
 {4650.0000, 1674.77},
 {4810.3448, 1769.45},
 {4970.6897, 1864.50},
 {5131.0345, 1959.52},
 {5291.3793, 2054.19},
 {5451.7242, 2148.34},
 {5612.0690, 2241.81},
 {5772.4138, 2334.46},
 {5932.7587, 2426.18},
 {6093.1035, 2516.84},
 {6253.4483, 2606.28},
 {6413.7932, 2694.36},
 {6574.1380, 2780.89},
 {6734.4828, 2865.70},
 {6894.8277, 2948.59},
 {7055.1725, 3029.32},
 {7215.5173, 3107.70},
 {7375.8622, 3183.46},
 {7536.2070, 3256.36},
 {7696.5518, 3326.12},
 {7856.8967, 3392.45},
 {8017.2415, 3455.01},
 {8177.5863, 3513.44},
 {8337.9312, 3567.37},
 {8498.2760, 3616.34},
 {8658.6208, 3659.87},
 {8818.9657, 3697.42},
 {8979.3105, 3728.38},
 {9139.6553, 3752.11},
 {9300.0002, 3767.93},
 {   0.0000 ,   0.00}
};

/* XXX: Dummy values from the User Manual for the NFM. Update for production. */
static const muse_specres_lut_entry
muse_specres_data_nfm[] =
{
 {4800.0000, 1740.00},
 {9300.0000, 3450.00},
 {   0.0000 ,   0.00}
};

/* Instrument properties and reference values */

/* Number of detector readout ports */
static const unsigned int kMuseNumQuadrants = 4;

/* Nominal field size [arcsec]  */
static const double kMuseFovSizeWFM = 60.00;
static const double kMuseFovSizeNFM =  7.43;

/* Nominal pixel scale wfm, nfm */
static const double kMusePixscaleWFM = 0.2;
static const double kMusePixscaleNFM = 0.025;

/* Reference wavelength [Angstrom] */
static const double kMuseLambdaRef = 7000.;

/* Instrument response at reference wavelength */
static const double kMuseResponseRef = 41.2;

/* Typical error estimated from unresolved sources [arcsec] */
static const double kMuseExtraErrorIQE = 0.3;

/* Typical values [arcsec] for the effective spatial resolution */
/* and its uncertainty, given by the median and the standard    */
/* deviation of previous measurements.                          */
static const double kMuseSkyRes      = 0.853823;
static const double kMuseSkyResError = 0.495547;


/* Unit conversion factors */

static const double day2sec     = 86400.0;     /* Seconds per day       */
static const double m2nm        = 1.e9;        /* meter to nanometer    */
static const double nm2Angstrom = 10.;         /* nanometer to Angstrom */
static const double deg2as      = 3600.;       /* degrees to arcseconds */


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

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    find out the value of the flux calibration flag
  @param    aHeaders       property list/headers to read from
  @return   the requested value or CPL_FALSE on error

  Queries FITS header for ESO.DRS.MUSE.PIXTABLE.FLUXCAL
 */
/*----------------------------------------------------------------------------*/
static cpl_boolean
muse_pfits_get_fluxcal(const cpl_propertylist *aHeaders)
{
  cpl_ensure(aHeaders, CPL_ERROR_NULL_INPUT, CPL_FALSE);
  cpl_errorstate prestate = cpl_errorstate_get();
  cpl_boolean value = cpl_propertylist_get_bool(aHeaders,
                                               MUSE_HDR_PT_FLUXCAL);
  cpl_errorstate_set(prestate);
  return value;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    Get the pixel scale factors in units of arcseconds.
  @param    aHeader   Property list/headers to read from
  @param    xscale    Pixel scale along the x-axis
  @param    yscale    Pixel scale along the y-axis
  @return   0 if the returned scales are obtained from measured values and 1 if
            only the nominal pixel scales are available. If none of them can be
            determined, i.e. the instrument mode is not available, -1 is
            returned and a CPL_ERROR_DATA_NOT_FOUND error is set.

  Retrieves the x and y pixel scale factors from the header using
  muse_wcs_get_scales() and converts them to arcseconds. If the pixel scale
  information is not available in the header, the nominal pixel scales,
  according to the current instrument mode, are returned.
 */
/*----------------------------------------------------------------------------*/
static int
muse_idp_get_scales(const cpl_propertylist *aHeader, double *xscale, double *yscale)
{

  int flag = 0;

  double _xscale = 0.;
  double _yscale = 0.;

  cpl_errorstate status = cpl_errorstate_get();

  if (muse_wcs_get_scales((cpl_propertylist *)aHeader, &_xscale, &_yscale) != CPL_ERROR_NONE) {
    const char *insmode = muse_pfits_get_insmode(aHeader);
    if (insmode) {
      if (strncmp(insmode, "NFM", 3) ==  0) {
        _xscale = kMusePixscaleNFM;
        _yscale = kMusePixscaleNFM;
      } else {
        _xscale = kMusePixscaleWFM;
        _yscale = kMusePixscaleWFM;
      }
      flag = 1;
    } else {
      *xscale = _xscale;
      *yscale = _yscale;
      flag = -1;
    }
  } else {
    _xscale *= deg2as;
    _yscale *= deg2as;
  }

  cpl_errorstate_set(status);

  if (flag == -1) {
    cpl_msg_debug(__func__, "Keyword \"INS.MODE\" is missing!");
    cpl_error_set(__func__, CPL_ERROR_DATA_NOT_FOUND);
  } else {
    *xscale = _xscale;
    *yscale = _yscale;
  }

  return flag;
}


/*----------------------------------------------------------------------------*/
/**
  @private
  @brief Compute spectral resolution for the central wavelength.
  @param aLambda      Wavelength in Angstrom.
  @param aSpecresLut  Spectral resolution look-up table.
  @return The interpolated spectral resolution

  The function computes the spectral resolution at the wavelength @em aLambda,
  by interpolating the data points given by the look-up table @em aSpecresLut.
  For wavelengths outside of the wavelength range of the table, the lower, or
  the upper boundary value is returned, respectively.
 */
/*----------------------------------------------------------------------------*/
static double
muse_idp_compute_specres(double aLambda, const muse_specres_lut_entry *aSpecresLut)
{

  /* Locate the appropriate wavelength bin in the lut and compute the *
   * spectral by linear interpolation.                                */

  cpl_size i = 0;
  while ((aSpecresLut[i].lambda > 0.) && (aLambda > aSpecresLut[i].lambda)) {
    ++i;
  }

  if (i == 0) {
    return aSpecresLut[0].R;
  }
  else if (aSpecresLut[i].lambda == 0.) {
    return aSpecresLut[i - 1].R;
  }

  double t = (aLambda - aSpecresLut[i - 1].lambda) /
      (aSpecresLut[i].lambda - aSpecresLut[i - 1].lambda);

  return (1 - t) * aSpecresLut[i - 1].R + t * aSpecresLut[i].R;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief Estimate image quality at a given wavelength from the seeing
         and the airmass.
  @param aLambda   Wavelength in Angstrom at which the image quality is
                   computed.
  @param aSeeing   The seeing as seen by the telescope and corrected for
                   airmass.
  @param aAirmass  Airmass at which the observation was carried out.
  @return The estimate of the image quality at the given wavelength.

  The returned image quality estimate is in units of arcsec.
 */
/*----------------------------------------------------------------------------*/
static double
muse_idp_compute_iqe(double aLambda, double aSeeing, double aAirmass)
{

  const double rad2as = 180. / CPL_MATH_PI * 3600.;  /* radians to arcsec */


  /* Wavefront outer scale at Paranal [meters] */
  const double L0 = 23.;

  /* UT M1 diameter [meters] from UT M1 vignetted area */
  const double D  = 8.1175365721399437;

    /* Kolb factor (cf. ESO Technical Report 12) */
  const double Fkolb = 1. / (1. + 300. * D / L0) - 1.;


  /* Scaled wavelength: lambda over lambda_ref */
  double lambda = aLambda / 5000.;

  /* Fried parameter */
  double r0 = 0.976 * 5.e-7 / aSeeing * rad2as *
      pow(lambda, 1.2) * pow(aAirmass, -0.6);

  double iqe = aSeeing * pow(lambda, -0.2) * pow(aAirmass, 0.6);
  iqe *= sqrt(1. + Fkolb * 2.183 * pow(r0 / L0, 0.356));

  return iqe;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief Compute pixel-to-pixel variance estimate from a datacube.
  @param aCube        Data cube for which the variance estimate is computed.
  @param aVarianceMin Lower variance limit of the histogram.
  @param aVarianceMax Upper variance limit of the histogram.
  @param aVarianceBin Variance histogram bin size.
  @return The estimate of the pixel-to-pixel variance.

  Creates a histogram of variances from the data cube STAT extension for the
  given minimum and maximum variance values and the given histogram bin size.
  Bad pixels encoded as NaN values are ignored.
  The pixel-to-pixel variance is estimated as the mode of the histogram.
 */
/*----------------------------------------------------------------------------*/
static double
muse_idp_compute_pixnoise(const muse_datacube *aCube,
                          double aVarianceMin, double aVarianceMax,
                          double aVarianceBin)
{
  cpl_ensure(aCube != NULL, CPL_ERROR_NULL_INPUT, 0.);
  cpl_ensure(aCube->stat, CPL_ERROR_DATA_NOT_FOUND, 0.);
  cpl_ensure((aVarianceMin >= 0.) && (aVarianceMax > aVarianceMin),
             CPL_ERROR_ILLEGAL_INPUT, 0.);
  cpl_ensure(aVarianceBin > 0., CPL_ERROR_ILLEGAL_INPUT, 0.);

  cpl_size nbins = (cpl_size)((aVarianceMax - aVarianceMin) / aVarianceBin) + 1;
  if (nbins < 2) {
    cpl_error_set(__func__, CPL_ERROR_INCOMPATIBLE_INPUT);
    return 0.;
  }

  const cpl_imagelist *data = aCube->stat;
  cpl_matrix *histogram = cpl_matrix_new(1, nbins);
  double *hdata = cpl_matrix_get_data(histogram);

  cpl_size iplane;
  for (iplane = 0; iplane < cpl_imagelist_get_size(data); ++iplane) {
    const cpl_image *plane = cpl_imagelist_get_const(data, iplane);
    const float *_data = cpl_image_get_data_float_const(plane);
    cpl_size ndata = cpl_image_get_size_x(plane) * cpl_image_get_size_y(plane);
    cpl_size idata;
    for (idata = 0; idata < ndata; ++idata) {
      register double v = _data[idata];
      if ((isnan(v) == 0) && (v >= aVarianceMin) && (v <= aVarianceMax)) {
        cpl_size ibin = (cpl_size)((v + aVarianceMin) / aVarianceBin);
        hdata[ibin] += 1.;
      }
    }
  }

  /* Get position of the histogram maximum. Since the histogram is a 1D *
   * vector, the row position is just a dummy.                          */
  cpl_size irow = 0.;
  cpl_size icol = 0.;
  cpl_matrix_get_maxpos(histogram, &irow, &icol);

  /* Use center of the histogram bin */
  double mode = aVarianceMin + (icol + 0.5) * aVarianceBin;
  cpl_matrix_delete(histogram);

  return mode;
}

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief Compute the AB magnitude limit
  @param aCube    Data cube for which the magnitude limit is computed.
  @param aWlenMin Lower limit of the wavelength range [Angstrom].
  @param aWlenMax Upper limit of the wavelength range [Angstrom].
  @param aFwhm    FWHM to use for the convolution filter.
  @return The estimate of the AB magnitude limit.
 */
/*----------------------------------------------------------------------------*/
static double
muse_idp_compute_abmaglimit(const muse_datacube *aCube, double aWlenMin,
                            double aWlenMax, double aFwhm)
{
  cpl_ensure(aCube != NULL, CPL_ERROR_NULL_INPUT, 0.);
  cpl_ensure(aWlenMax > aWlenMin, CPL_ERROR_ILLEGAL_INPUT, 0.);
  cpl_ensure(aFwhm > 0., CPL_ERROR_ILLEGAL_INPUT, 0.);

  /* Minimum number of valid data pixel needed to perform the computation  *
   * of the magnitude limit. This is given by using the standard deviation *
   * in the computation of the limiting magnitude                          */
  const cpl_size min_valid = 2;

  /* Create white light image and encode all invalid pixels as NaN for *
   * the following analysis.                                           */
  muse_image *fov = muse_datacube_collapse((muse_datacube *)aCube, NULL);
  muse_image_dq_to_nan(fov);

  cpl_size nx = cpl_image_get_size_x(fov->data);
  cpl_size ny = cpl_image_get_size_y(fov->data);

  /* Get median pixel value ignoring bad pixel values (NaN) */
  cpl_image_reject_value(fov->data, CPL_VALUE_NOTFINITE);
#undef USE_HISTOGRAM
#ifndef USE_HISTOGRAM
  double median = cpl_image_get_median(fov->data);
#else
  double sigma = 0;
  double median = cpl_image_get_mad(fov->data, &sigma);
#endif


  /* Create cleaned FOV image from the original image using only       *
   * pixels which have less intensity than the median. All invalid     *
   * pixels are recorded in a mask image. To prepare for the following *
   * convolution step the image is also cropped to an even number of   *
   * pixels along both axes (if needed).                               */
  cpl_size _nx = (nx % 2) == 0 ? nx : nx - 1;
  cpl_size _ny = (ny % 2) == 0 ? ny : ny - 1;
  cpl_size npixel = _nx * _ny;

  cpl_image *fov_clean = cpl_image_new(_nx, _ny, CPL_TYPE_DOUBLE);
  cpl_image_fill_window(fov_clean, 1, 1, _nx, _ny, median);

  cpl_mask *fov_invalid  = cpl_mask_new(_nx, _ny);
  cpl_binary *_fov_invalid = cpl_mask_get_data(fov_invalid);

  const float *_fov_data = cpl_image_get_data_float(fov->data);
  double *_fov_clean = cpl_image_get_data_double(fov_clean);

  cpl_size iy;
  for (iy = 0; iy < _ny; ++iy) {
    cpl_size ix;
    for (ix = 0; ix < _nx; ++ix) {
      cpl_size _ipixel = nx * iy + ix;
      if ((isnan(_fov_data[_ipixel]) == 0) &&
          (_fov_data[_ipixel] < median)) {
        _fov_clean[_nx * iy + ix] = _fov_data[_ipixel];
      } else {
        _fov_invalid[_nx * iy + ix] = CPL_BINARY_1;
      }

    }
  }
  muse_image_delete(fov);

  /* Convolve the valid image with a Gaussian normalized to 1 at the peak.  *
   * muse_matrix_new_gaussian_2d() creates a 2d Gaussian normalized such    *
   * the integral (the total sum of the elements) is 1. Thus it has to be   *
   * renormalized. The half width (radius) of the convolution box is set to *
   * 3 sigma.                                                             */
  const double csigma = aFwhm / (2. * sqrt(2. * log(2.)));
  const double radius = CPL_MAX(3. * csigma, 2.001);
  const int nhalf = (int)(radius + 0.5);
  cpl_matrix *ckernel = muse_matrix_new_gaussian_2d(nhalf, nhalf, csigma);
  cpl_matrix_divide_scalar(ckernel, cpl_matrix_get_max(ckernel));

  cpl_image *fov_smooth = muse_convolve_image(fov_clean, ckernel);

  cpl_matrix_delete(ckernel);
  cpl_image_delete(fov_clean);

  /* Perform a morphological dilation on the mask to create an additional  *
   * border of masked pixels around the already masked areas. This should  *
   * invalidated pixels in the smoothed image which are affected by border *
   * effects during the convolution.                                       */

  cpl_size nmask = 2 * nhalf + 1;
  cpl_mask *mkernel = cpl_mask_new(nmask, nmask);
  cpl_binary *_mkernel = cpl_mask_get_data(mkernel);
  double rsquare = radius * radius;

  for (iy = 0; iy < nmask; ++iy) {
    cpl_size ix;
    double y = (double)iy - radius;
    for (ix = 0; ix < nmask; ++ix) {
      double x = (double)ix - radius;
      _mkernel[nmask * iy + ix] = ((x * x + y * y) > rsquare) ?
          CPL_BINARY_0 : CPL_BINARY_1;
    }
  }

  cpl_mask *fov_mask = cpl_mask_new(_nx, _ny);
  cpl_mask_filter(fov_mask, fov_invalid, mkernel,
                  CPL_FILTER_DILATION, CPL_BORDER_ZERO);
  cpl_mask_delete(mkernel);

  /* The filtering of the mask leaves an nhalf pixel wide border on each   *
   * side of the mask image which are not flagged. These are set as masked *
   * here.                                                                 *
   */
  cpl_binary *_fov_mask = cpl_mask_get_data(fov_mask);

  cpl_size ipixel;
  for (ipixel = 0; ipixel < npixel; ++ipixel) {
    cpl_size _x = ipixel % _nx;
    cpl_size _y = ipixel / _nx;
    if (((_x < nhalf) ||(_x > (_nx - nhalf - 1))) ||
        ((_y < nhalf) ||(_y > (_ny - nhalf - 1)))) {
      _fov_mask[_y * _nx + _x] = CPL_BINARY_1;
    }
  }

  /* Check whether enough valid pixels are left for the following analysis. *
   * If not enough valid pixels are found revert the dilated mask to the    *
   * original and use this instead.                                         */
  if (npixel - cpl_mask_count(fov_mask) < min_valid) {
    cpl_msg_debug(__func__, "Number of valid pixels in the dilated mask is"
                  "less than 2! Falling back to the original mask!");
    cpl_mask_delete(fov_mask);
    fov_mask = fov_invalid;
    fov_invalid = NULL;
  } else {
    cpl_mask_delete(fov_invalid);
  }

#ifndef USE_HISTOGRAM
  const double *_fov_smooth = cpl_image_get_data_double_const(fov_smooth);

  double *_fov_valid = cpl_calloc(npixel, sizeof *_fov_valid);

  cpl_size nvalid = 0;
  for (ipixel = 0; ipixel < npixel; ++ipixel) {
    if (_fov_mask[ipixel] == CPL_BINARY_0) {
      _fov_valid[nvalid] = _fov_smooth[ipixel];
      ++nvalid;
    }
  }
  cpl_mask_delete(fov_mask);
  cpl_image_delete(fov_smooth);

  cpl_vector *fov_valid = cpl_vector_wrap(nvalid, _fov_valid);
  double sdev = cpl_vector_get_stdev(fov_valid);

  cpl_vector_unwrap(fov_valid);
  cpl_free(_fov_valid);

  /* Compute the AB limiting magnitude: Numerical values are from     *
   * converting f(nu) to f(lambda) and from the unit conversion [Jy]  *
   * to [erg/cm**2/s/Angstrom]:                                       *
   *    f(nu) = lambda**2 / c * f(lambda),                            *
   *    f(nu)/Jy = 3.34e4 * (lambda/Angstrom)**2 *                    *
   *               f(lambda)/(erg/cm**2/s/Angstrom)                   *
   * and the monochromatic AB magnitude                               *
   *    m_AB = -2.5 * log10(f(nu) / (3631 Jy))                        */

  double zeropoint  = -2.5 * log10(5. * (3.34e4 * aWlenMin * aWlenMax / 3631.));
  double abmaglimit = -2.5 * log10(2. * sdev / kMuseFluxUnitFactor) + zeropoint;
#else
  /* Create histogram from the smoothed image of valid pixels. The bin size *
   * is chosen to be 0.1 times the median absolute deviation of the pixel   *
   * values of the original image                                           */
  const double hstep = 0.1 * sigma;
  double hmin  = cpl_image_get_min(fov_smooth);
  double hmax  = cpl_image_get_max(fov_smooth);

  cpl_size nbins = (hmax - hmin) / hstep + 1;
  cpl_vector *bins = cpl_vector_new(nbins);

  /* To prepare for the following fit of the histogram peak use the bin *
   * center as the value of the histogram bin                           */
  cpl_size ibin;
  for (ibin = 0; ibin < nbins; ++ ibin) {
    cpl_vector_set(bins, ibin, hmin + (ibin + 0.5) * hstep);
  }

  cpl_vector *histogram = cpl_vector_new(nbins);
  cpl_vector_fill(histogram, 0.);
  double *hdata = cpl_vector_get_data(histogram);

  const double *_fov_smooth = cpl_image_get_data_double_const(fov_smooth);

  for (ipixel = 0; ipixel < npixel; ++ipixel) {
    if ((_fov_mask[ipixel] == CPL_BINARY_0) &&
        (_fov_smooth[ipixel] >= hmin) && (_fov_smooth[ipixel] <= hmax)) {
      cpl_size jbin = (cpl_size)((_fov_smooth[ipixel] + fabs(hmin)) / hstep);
      hdata[jbin] += 1.;
    }
  }

  cpl_mask_delete(fov_mask);
  cpl_image_delete(fov_smooth);

  /* Fit a Gaussian to the histogram peak */
  double center  = 0.;
  double sdev    = 0.;
  double area    = 0.;
  double bckgrnd = 0.;

  cpl_fit_mode mode = CPL_FIT_CENTROID | CPL_FIT_STDEV | CPL_FIT_AREA;
  cpl_errorstate status = cpl_errorstate_get();
  cpl_error_code ecode  = cpl_vector_fit_gaussian(bins, NULL, histogram, NULL,
                                                  mode,
                                                  &center, &sdev, &area, &bckgrnd,
                                                  NULL, NULL, NULL);
  cpl_vector_delete(histogram);
  cpl_vector_delete(bins);

  double abmaglimit = 0.;
  if (!cpl_errorstate_is_equal(status) && (ecode != CPL_ERROR_CONTINUE)) {
    cpl_msg_debug(__func__, "Fit of a Gaussian to the histogram peak failed!");
    cpl_errorstate_set(status);
  } else {
    if (ecode == CPL_ERROR_CONTINUE) {
      cpl_msg_debug(__func__, "Fit of a Gaussian to the histogram peak did "
                    "not converge!");
      cpl_errorstate_set(status);
    }

    /* Compute the AB limiting magnitude: Numerical values are from     *
     * converting f(nu) to f(lambda) and from the unit conversion [Jy]  *
     * to [erg/cm**2/s/Angstrom]:                                       *
     *    f(nu) = lambda**2 / c * f(lambda),                            *
     *    f(nu)/Jy = 3.34e4 * (lambda/Angstrom)**2 *                    *
     *               f(lambda)/(erg/cm**2/s/Angstrom)                   *
     * and the monochromatic AB magnitude                               *
     *    m_AB = -2.5 * log10(f(nu) / (3631 Jy))                        */

    double zeropoint  = -2.5 *
        log10(5. * (3.34e4 * aWlenMin * aWlenMax / 3631.));
    abmaglimit = -2.5 * log10(2. * sdev / kMuseFluxUnitFactor) + zeropoint;
  }
#endif

  return abmaglimit;
}



muse_idp_properties *
muse_idp_properties_new(void) {
  muse_idp_properties *properties = cpl_calloc(1, sizeof *properties);
  return properties;
}

void
muse_idp_properties_delete(muse_idp_properties *aProperties)
{
  if (aProperties) {
    cpl_array_delete(aProperties->obid);
    cpl_array_delete(aProperties->progid);
    cpl_propertylist_delete(aProperties->prov);
    cpl_array_delete(aProperties->asson);

    /* TODO: Remove deallocator call for assoc once it has been *
     * removed from the interface.                              */
    cpl_array_delete(aProperties->assoc);
    cpl_free((char *)aProperties->prodcatg);
    cpl_free((char *)aProperties->procsoft);
    cpl_free((char *)aProperties->obstech);
    cpl_free((char *)aProperties->referenc);
  }
  cpl_free(aProperties);
  return;
}


muse_idp_properties *
muse_idp_properties_collect(muse_processing *aProcessing,
                            const muse_datacube *aCube,
                            const char *aTag)
{
  cpl_ensure(aProcessing, CPL_ERROR_NULL_INPUT, NULL);
  cpl_table *exposures = muse_processing_sort_exposures(aProcessing);
  cpl_ensure(exposures, CPL_ERROR_DATA_NOT_FOUND, NULL);
  cpl_size nexposures = cpl_table_get_nrow(exposures);

  muse_idp_properties *properties = muse_idp_properties_new();
  properties->obid    = cpl_array_new(nexposures, CPL_TYPE_LONG);
  properties->progid  = cpl_array_new(nexposures, CPL_TYPE_STRING);
  properties->prov    = cpl_propertylist_new();
  properties->fluxcal = CPL_TRUE;
  properties->wlerror = 0.026;     /* Fallback random error estimate of wavelength */

  /* Collect data from the raw data files */

  cpl_errorstate status = cpl_errorstate_get();

  cpl_size kexposure = 0;
  cpl_size iexposure;
  double fwhmmin = DBL_MAX;
  double fwhmmax = -DBL_MAX;

  for (iexposure = 0; iexposure < nexposures; ++iexposure) {
    const char *filename = NULL;
    if (cpl_table_has_column(exposures, "00")) {
      filename = cpl_table_get_string(exposures, "00", iexposure);
    }
    if (!filename) {
      int ifu=1;
      while (!filename && (ifu <= kMuseNumIFUs)) {
        char *column = cpl_sprintf("%02d", ifu);
        filename = cpl_table_get_string(exposures, column, iexposure);
        cpl_free(column);
        ++ifu;
      }
    }
    if (!filename) {
      cpl_msg_error(__func__, "No pixel table is available for exposure %"
                    CPL_SIZE_FORMAT " of %" CPL_SIZE_FORMAT "!",
                    iexposure + 1, nexposures);
      cpl_error_set(__func__, CPL_ERROR_DATA_NOT_FOUND);
      break;
    } else {

      cpl_msg_debug(__func__, "Collecting IDP data from exposure '%s'",
                    filename);
      cpl_propertylist *_header = cpl_propertylist_load(filename, 0);

      const char *progid = muse_pfits_get_progid(_header);
      long obid = muse_pfits_get_obsid(_header);
      double mjd     = muse_pfits_get_mjdobs(_header);
      double exptime = muse_pfits_get_exptime(_header);
      double endtime = mjd + exptime / day2sec;

      /* Estimate the number of illuminated pixels from the nominal size *
       * of the field-of-view in pixels.                                 */
      double npixel = 0.;

      cpl_errorstate _status = cpl_errorstate_get();

      const char *_insmode = muse_pfits_get_insmode(_header);
      if (_insmode) {
        if (strncmp(_insmode, "NFM", 3) ==  0) {
          npixel = (kMuseFovSizeNFM * kMuseFovSizeNFM) /
              (kMusePixscaleNFM * kMusePixscaleNFM);
        } else {
          npixel = (kMuseFovSizeWFM * kMuseFovSizeWFM) /
              (kMusePixscaleWFM * kMusePixscaleWFM);
        }
      }

      if (!cpl_errorstate_is_equal(_status)) {
        if (cpl_error_get_code() == CPL_ERROR_DATA_NOT_FOUND) {
          cpl_msg_debug(__func__, "Keyword \"INS.MODE\" is missing in '%s'!",
                        filename);
        }
        cpl_msg_error(__func__, "Cannot determine pixel scales of exposure '%s'",
                      filename);
        cpl_propertylist_delete(_header);
        break;
      }

      properties->exptime += exptime * npixel;
      properties->texptime += exptime;
      properties->mjd_end = CPL_MAX(endtime, properties->mjd_end);

      /* Get image quality (at the reference wavelength) and the associated *
       * error for this exposure                                            */
      double zstart = muse_pfits_get_airmass_start(_header);
      double zend   = muse_pfits_get_airmass_end(_header);

      _status = cpl_errorstate_get();
      double fwhmstart = muse_pfits_get_fwhm_start(_header);
      double fwhmend   = muse_pfits_get_fwhm_end(_header);

      if (!cpl_errorstate_is_equal(_status)) {
          cpl_msg_debug(__func__, "Missing or invalid seeing information "
                  "(\"TEL.AMBI.FWHM.START\", \"TEL.AMBI.FWHM.END\") in "
                  "exposure '%s'", filename);
          cpl_errorstate_set(_status);
      }

      double iqe    = kMuseSkyRes;
      double fwhmIA = muse_pfits_get_ia_fwhmlin(_header);

      if (cpl_errorstate_is_equal(_status)) {
        iqe = muse_idp_compute_iqe(kMuseLambdaRef, fwhmIA,
                                   0.5 * (zstart + zend));
      }
      else {
        /* Ignore the error and use the default constant instead. */
        /* Set fwhmstart and fwhmend to 0. This forces fwhmmin to */
        /* zero and the uncertainty of SKY_RES to become the      */
        /* default constant.                                      */
        cpl_msg_debug(__func__, "Missing or invalid seeing information "
                "(\"TEL.IA.FWHMLIN\") in exposure '%s'", filename);
        cpl_errorstate_set(_status);
        fwhmstart = 0.;
        fwhmend   = 0.;
      }

      properties->skyres += exptime * iqe;

      fwhmmin = CPL_MIN(fwhmmin, CPL_MIN(fwhmstart, fwhmend));
      fwhmmax = CPL_MAX(fwhmmax, CPL_MAX(fwhmstart, fwhmend));

      /* Observation ID, program ID, etc. */
      cpl_array_set_long(properties->obid, kexposure, obid);
      if (progid) {
        cpl_array_set_string(properties->progid, kexposure, progid);
      }

      if ((muse_pfits_get_fluxcal(_header) == CPL_FALSE) &&
          (cpl_frameset_find(aProcessing->usedframes, MUSE_TAG_STD_RESPONSE) == NULL)) {
        properties->fluxcal = CPL_FALSE;
      }

      unsigned int nraw = cpl_propertylist_get_size(properties->prov);
      const char *prov = muse_pfits_get_ancestor(_header);
      if (!prov) {
        prov = muse_pfits_get_arcfile(_header);
        if (!prov) {
          prov = muse_pfits_get_origfile(_header);
        }
      }

      if (prov) {
        char *name = cpl_sprintf("PROV%-u", ++nraw);
        cpl_propertylist_append_string(properties->prov, name, prov);
        cpl_free(name);
      } else {
        unsigned int iraw = 1;
        prov = muse_pfits_get_raw_filename(_header, iraw);
        while (prov) {
          char *name = cpl_sprintf("PROV%-u", ++nraw);
          cpl_propertylist_append_string(properties->prov, name, prov);
          prov = muse_pfits_get_raw_filename(_header, ++iraw);
          cpl_free(name);
        }
      }

      cpl_propertylist_delete(_header);
      ++kexposure;
    }
  }
  cpl_table_delete(exposures);

  if (!cpl_errorstate_is_equal(status)) {
    muse_idp_properties_delete(properties);
    return NULL;
  }

  if (kexposure != nexposures) {
    cpl_msg_warning(__func__, "Could not collect raw data information of all "
                    "exposures. Got %" CPL_SIZE_FORMAT " of %" CPL_SIZE_FORMAT
                    "!", kexposure, nexposures);
  }

  properties->ncombine = kexposure;
  cpl_array_set_size(properties->obid, kexposure);
  cpl_array_set_size(properties->progid, kexposure);


  /* Collect ancillary products data */
  cpl_msg_debug(__func__, "Collecting IDP ancillary products information");

  cpl_size nframes = cpl_frameset_get_size(aProcessing->outframes);
  properties->asson = cpl_array_new(nframes, CPL_TYPE_STRING);

  cpl_size iancillary = 0;
  cpl_size nptotal = 0;
  cpl_size iframe;
  for (iframe = 0; iframe < nframes; ++iframe) {
    const cpl_frame *frame = cpl_frameset_get_position(aProcessing->outframes,
                                                       iframe);
    const char *tag = cpl_frame_get_tag(frame);

    if (strcmp(tag, MUSE_TAG_IMAGE_FOV) == 0) {
      cpl_array_set_string(properties->asson, iancillary,
                           cpl_frame_get_filename(frame));

      /* Estimate the number of illuminated pixels of the combined    *
       * field-of-view. The maximum number of valid pixels across all *
       * the created field-of-view images is used as this estimate.   */

      muse_image *_fov = muse_fov_load(cpl_frame_get_filename(frame));

      if (_fov) {
        float *pixels = cpl_image_get_data_float(_fov->data);
        cpl_size npix = cpl_image_get_size_x(_fov->data) * cpl_image_get_size_y(_fov->data) ;

        cpl_size _nptotal = 0;
        cpl_size ipix;
        for (ipix = 0; ipix < npix; ++ipix) {
          if (isnan(pixels[ipix]) == 0) {
            ++_nptotal;
          }
        }
        nptotal = CPL_MAX(nptotal, _nptotal);

        muse_image_delete(_fov);
      }
      ++iancillary;
    }
  }
  cpl_array_set_size(properties->asson, iancillary);

  /* Estimate exposure time per pixel. If the number of illuminated pixels *
   * cannot be computed, or if there are no "valid" pixels the exposure    *
   * time is forced to zero.                                               */

  if (nptotal == 0) {
    properties->exptime = 0.;
  } else {
    properties->exptime /= nptotal;
  }

  /* Compute product image quality estimate and its uncertainty */
  properties->skyres /= properties->texptime;
  if ((fwhmmin > 0.) && (fwhmmax > 0.)) {
      properties->skyrerr = 0.5 * fabs(fwhmmax - fwhmmin);
      properties->skyrerr = sqrt(properties->skyrerr * properties->skyrerr +
                                 kMuseExtraErrorIQE * kMuseExtraErrorIQE);
  } else {
      cpl_msg_warning(__func__, "Got invalid seeing information! Using an "
                      "estimate for \"SKY_RERR\"!");
      properties->skyrerr = kMuseSkyResError;
  }


  /* Collect data from the product header. */

  cpl_msg_debug(__func__, "Collecting IDP data from products");

  const cpl_propertylist *header = aCube->header;

  /* Get FOV center coordinates and the limits of wavelength range. */
  cpl_wcs *wcs = cpl_wcs_new_from_propertylist(header);
  int naxes = cpl_wcs_get_image_naxis(wcs);
  const cpl_array *naxis = cpl_wcs_get_image_dims(wcs);

  cpl_matrix *position = cpl_matrix_new(2, naxes);
  cpl_matrix_fill_column(position, 0.5 * cpl_array_get_int(naxis, 0, NULL), 0);
  cpl_matrix_fill_column(position, 0.5 * cpl_array_get_int(naxis, 1, NULL), 1);
  cpl_matrix_set(position, 0, 2, 1.);
  cpl_matrix_set(position, 1, 2, cpl_array_get_int(naxis, 2, NULL));

  cpl_matrix *world;
  cpl_array *flags;
  cpl_error_code _status = cpl_wcs_convert(wcs, position, &world, &flags,
                                           CPL_WCS_PHYS2WORLD);

  cpl_array_delete(flags);
  cpl_matrix_delete(position);

  if (_status != CPL_ERROR_NONE) {
    cpl_matrix_delete(world);
    cpl_wcs_delete(wcs);
    muse_idp_properties_delete(properties);
    return NULL;
  }

  properties->fovcenter[0] = cpl_matrix_get(world, 0, 0);
  properties->fovcenter[1] = cpl_matrix_get(world, 0, 1);

  // XXX: Check it there is anything extra that needs to be done for a
  //      AWAV-LOG axis
  properties->wlenrange[0] = cpl_matrix_get(world, 0, 2) * m2nm;
  properties->wlenrange[1] = cpl_matrix_get(world, 1, 2) * m2nm;
  cpl_matrix_delete(world);
  cpl_wcs_delete(wcs);

  /* Estimate the spectral resolution at the wavelength range center */
  const muse_specres_lut_entry *specres_data = muse_specres_data_wfm;

  const char *insmode = muse_pfits_get_insmode(header);
  if (strncmp(insmode, "NFM", 3) ==  0) {
    specres_data = muse_specres_data_nfm;
  }

  double wlencenter = 0.5 * (properties->wlenrange[0] + properties->wlenrange[1]);
  properties->specres = muse_idp_compute_specres(wlencenter * nm2Angstrom,
                                                 specres_data);

  /* Estimate the pixel-to-pixel noise from the statistical error of the *
   * data cube data.                                                     */

  status = cpl_errorstate_get();

  /* The arguments of the following call refer to variances and not sigmas. *
   * Thus, it considers uncertainties in the range -5 to 5 sigma, and a     *
   * histogram bin size of 0.2 sigma.                                       */

  double pixnoise = muse_idp_compute_pixnoise(aCube, 0, 25., 0.04);
  if (!cpl_errorstate_is_equal(status)) {
    double ron  = 0.;
    double gain = 0.;

    unsigned int iquadrant;
    for (iquadrant = 1; iquadrant <= kMuseNumQuadrants; ++iquadrant) {
      ron  += muse_pfits_get_ron(header, iquadrant);
      gain += muse_pfits_get_gain(header, iquadrant);
    }

    /* Here kMuseNumQuadrants^2 results from computing the mean ron and gain */
    properties->pixnoise = ron * gain * pow(10., -0.4 * kMuseResponseRef) /
        (double)(kMuseNumQuadrants * kMuseNumQuadrants);
    cpl_errorstate_set(status);
  } else {
    const char *bunit = muse_pfits_get_bunit(header);

    properties->pixnoise = sqrt(pixnoise);

    if (bunit &&
        !strncmp(bunit, kMuseFluxUnitString, strlen(kMuseFluxUnitString) + 1)) {
      properties->pixnoise /= kMuseFluxUnitFactor;
    } else {
      if (!bunit) {
        cpl_msg_warning(__func__,
                        "No BUNIT data units are present in the data cube!");
      } else {
        cpl_msg_warning(__func__,
                        "Data cube has unsupported data unit in BUNIT!");
      }
      cpl_msg_warning(__func__, "No scale factor has been applied to PIXNOISE!");
    }
  }

  /* Compute limiting magnitude */
  double xscale = 0.;
  double yscale = 0.;

  if (muse_idp_get_scales(header, &xscale, &yscale) == -1) {
    cpl_msg_error(__func__, "Cannot determine pixel scales of the data cube");
    muse_idp_properties_delete(properties);
    return NULL;
  }

  status = cpl_errorstate_get();
  double abmaglimit =
      muse_idp_compute_abmaglimit(aCube,
                                  properties->wlenrange[0] * nm2Angstrom,
                                  properties->wlenrange[1] * nm2Angstrom,
                                  properties->skyres / CPL_MAX(xscale, yscale));
  if (!cpl_errorstate_is_equal(status)) {
    properties->abmaglimit = -99.;
    cpl_errorstate_set(status);
  } else {
    properties->abmaglimit = abmaglimit;
  }

  /* Get the product category from the product header, unless it is *
   * overridden an explicitly given tag.                            */
  if (!aTag) {
    aTag = muse_pfits_get_pro_catg(header);
  }
  if (!aTag || (strcmp(aTag, MUSE_TAG_CUBE_FINAL) != 0)) {
    cpl_error_set(__func__, CPL_ERROR_ILLEGAL_INPUT);
    muse_idp_properties_delete(properties);
    return NULL;
  } else {
    properties->prodcatg = cpl_strdup(KEY_PRODCATG_VALUE_IFS_CUBE);
  }

  /* Get the most recently applied processing recipe of the product */
  unsigned int recid = 0;
  const char *procsoft = NULL;
  const char *pipeid;
  while ((pipeid = muse_pfits_get_pipe_id(header, ++recid)) != NULL) {
    procsoft = pipeid;
  }

  if (!procsoft) {
    properties->procsoft = cpl_strdup(PACKAGE "/" PACKAGE_VERSION);
  } else {
    properties->procsoft = cpl_strdup(procsoft);
  }

  /* The following IDP properties are simply hardcoded here since *
   * not supposed to change.                                      */
  properties->obstech  = cpl_strdup("IFU");
  properties->referenc = NULL;

  return properties;
}

cpl_error_code
muse_idp_properties_update(cpl_propertylist *aHeader,
                           const muse_idp_properties *aProperties)
{
  cpl_ensure(aHeader && aProperties, CPL_ERROR_NULL_INPUT, CPL_ERROR_NULL_INPUT);
  cpl_ensure(cpl_array_get_size(aProperties->obid) == aProperties->ncombine,
             CPL_ERROR_ILLEGAL_INPUT, CPL_ERROR_ILLEGAL_INPUT);
  cpl_ensure(cpl_array_get_size(aProperties->progid) == aProperties->ncombine,
             CPL_ERROR_ILLEGAL_INPUT, CPL_ERROR_ILLEGAL_INPUT);
  cpl_ensure(cpl_propertylist_get_size(aProperties->prov) >= aProperties->ncombine,
             CPL_ERROR_ILLEGAL_INPUT, CPL_ERROR_ILLEGAL_INPUT);

  cpl_propertylist_erase_regexp(aHeader, CLEAN_KEYS_REGEXP, 0);

  cpl_propertylist_update_double(aHeader, KEY_RA, aProperties->fovcenter[0]);
  cpl_propertylist_set_comment(aHeader, KEY_RA, KEY_RA_COMMENT);
  cpl_propertylist_update_double(aHeader, KEY_DEC, aProperties->fovcenter[1]);
  cpl_propertylist_set_comment(aHeader, KEY_DEC, KEY_DEC_COMMENT);
  cpl_propertylist_update_double(aHeader, KEY_EXPTIME, aProperties->exptime);
  cpl_propertylist_set_comment(aHeader, KEY_EXPTIME, KEY_EXPTIME_COMMENT);
  cpl_propertylist_insert_after_double(aHeader, KEY_EXPTIME,
                                       KEY_TEXPTIME, aProperties->texptime);
  cpl_propertylist_set_comment(aHeader, KEY_TEXPTIME, KEY_TEXPTIME_COMMENT);

  cpl_propertylist_insert_after_int(aHeader, KEY_TEXPTIME,
                                    KEY_NCOMBINE, aProperties->ncombine);
  cpl_propertylist_set_comment(aHeader, KEY_NCOMBINE, KEY_NCOMBINE_COMMENT);

  cpl_propertylist_set_comment(aHeader, KEY_MJDOBS, KEY_MJDOBS_COMMENT);
  cpl_propertylist_insert_after_double(aHeader, KEY_MJDOBS,
                                       KEY_MJDEND, aProperties->mjd_end);
  cpl_propertylist_set_comment(aHeader, KEY_MJDEND, KEY_MJDEND_COMMENT);

  cpl_array *obids = cpl_array_duplicate(aProperties->obid);
  muse_cplarray_sort(obids, CPL_TRUE);

  /* Add observation block IDs, skipping duplicates */
  long obid = cpl_array_get_long(obids, 0, NULL);
  cpl_propertylist_update_long(aHeader, KEY_OBID "1", obid);
  cpl_propertylist_set_comment(aHeader, KEY_OBID "1", KEY_OBID_COMMENT);

  if (aProperties->ncombine > 1) {
    unsigned int ik = 1;
    cpl_size idx;
    for (idx = 1; idx < aProperties->ncombine; ++idx) {
      long _obid = cpl_array_get_long(obids, idx, NULL);
      if (_obid != obid) {
        char *key = cpl_sprintf(KEY_OBID "%-u", ++ik);
        cpl_propertylist_update_long(aHeader, key, _obid);
        cpl_propertylist_set_comment(aHeader, key, KEY_OBID_COMMENT);
        cpl_free(key);
        obid = _obid;
      }
    }
  }
  cpl_array_delete(obids);

  /* Add the ESO program ID(s). If the product is made from a single program *
   * PROG_ID is the ID string, otherwise it set to the special value MULTI,  *
   * followed by a list of all IDs, skipping duplicate ID values             */
  cpl_array *progids = cpl_array_duplicate(aProperties->progid);
  muse_cplarray_sort(progids, CPL_TRUE);
  const char *progid = cpl_array_get_string(progids, 0);

  if (aProperties->ncombine > 1) {
    unsigned int nprogid = 1;
    cpl_size idx;
    for (idx = 1; idx < aProperties->ncombine; ++idx) {
      const char *_progid = cpl_array_get_string(progids, idx);
      if (strcmp(_progid, progid) != 0) {
        progid = _progid;
        ++nprogid;
      }
    }

    progid = cpl_array_get_string(progids, 0);
    if (nprogid == 1) {
        cpl_propertylist_update_string(aHeader, KEY_PROG_ID, progid);
    } else {
      cpl_propertylist_update_string(aHeader, KEY_PROG_ID,
                                     KEY_PROG_ID_VALUE_MULTIPLE);
      cpl_propertylist_update_string(aHeader, KEY_PROGID "1", progid);
      cpl_propertylist_set_comment(aHeader, KEY_PROGID "1", KEY_PROGID_COMMENT);

      unsigned int ik = 1;
      for (idx = 1; idx < aProperties->ncombine; ++idx) {
        const char *_progid = cpl_array_get_string(progids, idx);
        if (strcmp(_progid, progid) != 0) {
          char *key = cpl_sprintf(KEY_PROGID "%-u", ++ik);
          cpl_propertylist_update_string(aHeader, key, _progid);
          cpl_propertylist_set_comment(aHeader, key, KEY_PROGID_COMMENT);
          cpl_free(key);
          progid = _progid;
        }
      }
    }
    cpl_propertylist_set_comment(aHeader, KEY_PROG_ID, KEY_PROG_ID_COMMENT);

  } else {
    cpl_propertylist_update_string(aHeader, KEY_PROG_ID, progid);
    cpl_propertylist_set_comment(aHeader, KEY_PROG_ID, KEY_PROG_ID_COMMENT);
  }
  cpl_array_delete(progids);

  /* Add raw data information */
  cpl_propertylist_append(aHeader, aProperties->prov);

  /* Add ancillary products file information */
  cpl_size idx;
  for (idx = 0; idx < cpl_array_get_size(aProperties->asson); ++idx) {
    char *name = cpl_sprintf(KEY_ASSON "%-" CPL_SIZE_FORMAT, idx + 1);
    cpl_propertylist_update_string(aHeader, name,
                                   cpl_array_get_string(aProperties->asson, idx));
    cpl_free(name);
  }

  cpl_propertylist_update_string(aHeader, KEY_PRODCATG, aProperties->prodcatg);
  cpl_propertylist_set_comment(aHeader, KEY_PRODCATG, KEY_PRODCATG_COMMENT);

  cpl_propertylist_update_string(aHeader, KEY_PROCSOFT, aProperties->procsoft);
  cpl_propertylist_set_comment(aHeader, KEY_PROCSOFT, KEY_PROCSOFT_COMMENT);

  cpl_propertylist_update_string(aHeader, KEY_OBSTECH, aProperties->obstech);
  cpl_propertylist_set_comment(aHeader, KEY_OBSTECH, KEY_OBSTECH_COMMENT);

  const char *fluxcal = KEY_FLUXCAL_VALUE_TRUE;
  if (aProperties->fluxcal == CPL_FALSE) {
    fluxcal = KEY_FLUXCAL_VALUE_FALSE;
  }
  cpl_propertylist_update_string(aHeader, KEY_FLUXCAL, fluxcal);
  cpl_propertylist_set_comment(aHeader, KEY_FLUXCAL, KEY_FLUXCAL_COMMENT);

  cpl_propertylist_insert_after_double(aHeader, KEY_FLUXCAL, KEY_WAVELMIN,
                                       aProperties->wlenrange[0]);
  cpl_propertylist_set_comment(aHeader, KEY_WAVELMIN, KEY_WAVELMIN_COMMENT);
  cpl_propertylist_insert_after_double(aHeader, KEY_WAVELMIN, KEY_WAVELMAX,
                                       aProperties->wlenrange[1]);
  cpl_propertylist_set_comment(aHeader, KEY_WAVELMAX, KEY_WAVELMAX_COMMENT);
  cpl_propertylist_insert_after_double(aHeader, KEY_WAVELMAX, KEY_SPEC_RES,
                                       aProperties->specres);
  cpl_propertylist_set_comment(aHeader, KEY_SPEC_RES, KEY_SPEC_RES_COMMENT);
  cpl_propertylist_insert_after_double(aHeader, KEY_SPEC_RES, KEY_SKY_RES,
                                       aProperties->skyres);
  cpl_propertylist_set_comment(aHeader, KEY_SKY_RES, KEY_SKY_RES_COMMENT);
  cpl_propertylist_insert_after_double(aHeader, KEY_SKY_RES, KEY_SKY_RERR,
                                       aProperties->skyrerr);
  cpl_propertylist_set_comment(aHeader, KEY_SKY_RERR, KEY_SKY_RERR_COMMENT);
  cpl_propertylist_insert_after_double(aHeader, KEY_SKY_RERR, KEY_PIXNOISE,
                                       aProperties->pixnoise);
  cpl_propertylist_set_comment(aHeader, KEY_PIXNOISE, KEY_PIXNOISE_COMMENT);
  cpl_propertylist_insert_after_double(aHeader, KEY_PIXNOISE, KEY_ABMAGLIM,
                                       aProperties->abmaglimit);
  cpl_propertylist_set_comment(aHeader, KEY_ABMAGLIM, KEY_ABMAGLIM_COMMENT);

  const char *reference = "";
  if (aProperties->referenc) {
    reference = aProperties->referenc;
  }
  cpl_propertylist_update_string(aHeader, KEY_REFERENC, reference);
  cpl_propertylist_set_comment(aHeader, KEY_REFERENC, KEY_REFERENC_COMMENT);

  cpl_propertylist_insert_after_double(aHeader, "CRVAL3", KEY_SPEC_ERR,
                                       aProperties->wlerror);
  cpl_propertylist_set_comment(aHeader, KEY_SPEC_ERR, KEY_SPEC_ERR_COMMENT);

  /* Fallback required for IDPs */

  if (!cpl_propertylist_has(aHeader, "CSYER1")) {
    cpl_propertylist_update_double(aHeader, "CSYER1", -1.);
    cpl_propertylist_set_comment(aHeader, "CSYER1",
                                 "[deg] Systematic error in coordinate");
  }
  if (!cpl_propertylist_has(aHeader, "CSYER2")) {
    cpl_propertylist_update_double(aHeader, "CSYER2", -1.);
    cpl_propertylist_set_comment(aHeader, "CSYER2",
                                 "[deg] Systematic error in coordinate");
  }

  return CPL_ERROR_NONE;
}

cpl_error_code
muse_idp_properties_update_fov(muse_image *aFov)
{
  cpl_error_code status = CPL_ERROR_NONE;

  cpl_ensure(aFov, CPL_ERROR_NULL_INPUT, CPL_ERROR_NULL_INPUT);
  cpl_ensure(aFov->header, CPL_ERROR_ILLEGAL_INPUT, CPL_ERROR_ILLEGAL_INPUT);

  status = cpl_propertylist_update_string(aFov->header, KEY_PRODCATG,
                                           KEY_PRODCATG_VALUE_FOV_IMAGE);
  cpl_propertylist_set_comment(aFov->header, KEY_PRODCATG, KEY_PRODCATG_COMMENT);

  return status;
}

/**@}*/
