/* -*- 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-2017 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 <strings.h>

#include "muse_postproc.h"
#include "muse_instrument.h"

#include "muse_astro.h"
#include "muse_dar.h"
#include "muse_idp.h"
#include "muse_imagelist.h"
#include "muse_pfits.h"
#include "muse_phys.h"
#include "muse_quality.h"
#include "muse_raman.h"
#ifdef USE_LSF_PARAMS
# include "muse_sky_old.h"
#endif
#include "muse_utils.h"

#undef CREATE_IDP

/*----------------------------------------------------------------------------*/
/**
 * @defgroup muse_postproc     Post-processing functions
 *
 * This module contains functions that are common to some post-processing
 * recipes.
 */
/*----------------------------------------------------------------------------*/

/**@{*/

/*----------------------------------------------------------------------------*/
/**
  @brief   Create a post-processing properties object.
  @param   aType   the type of processing to set
  @return  the post-processing properties object or NULL on error

  In the returned structure, all pointer objects are NULL.
  The sky subtraction method is set to MUSE_POSTPROC_SKYMETHOD_NONE,
  the autocalibration is set to MUSE_POSTPROC_AUTOCALIB_NONE.

  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL, unsupported aType is given}
 */
/*----------------------------------------------------------------------------*/
muse_postproc_properties *
muse_postproc_properties_new(muse_postproc_type aType)
{
  muse_postproc_properties *prop = cpl_calloc(1, sizeof(muse_postproc_properties));
  switch (aType) {
  case MUSE_POSTPROC_SCIPOST:
  case MUSE_POSTPROC_STANDARD:
  case MUSE_POSTPROC_ASTROMETRY:
    prop->type = aType;
    break;
  default:
    cpl_msg_error(__func__, "No such setup known: %d", aType);
    cpl_error_set(__func__, CPL_ERROR_ILLEGAL_INPUT);
    cpl_free(prop);
    return NULL;
  }
  /* rvtype of 0 is already MUSE_RVCORRECT_NONE */
  return prop;
} /* muse_postproc_properties_new() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Free memory taken by a post-processing properties object and all its
           components.
  @param   aProp   the post-processing properties object
 */
/*----------------------------------------------------------------------------*/
void
muse_postproc_properties_delete(muse_postproc_properties *aProp)
{
  if (!aProp) {
    return;
  }
  cpl_table_delete(aProp->exposures);
  muse_table_delete(aProp->response);
  muse_table_delete(aProp->telluric);
  cpl_table_delete(aProp->extinction);
  cpl_propertylist_delete(aProp->wcs);
  muse_lsf_cube_delete_all(aProp->lsf_cube);
#ifdef USE_LSF_PARAMS
  muse_lsf_params_delete_all(aProp->lsf_params);
#endif
  muse_table_delete(aProp->autocal_table);
  cpl_table_delete(aProp->raman_lines);
  cpl_table_delete(aProp->sky_lines);
  cpl_table_delete(aProp->sky_continuum);
  muse_mask_delete(aProp->sky_mask);
  cpl_frame_delete(aProp->refframe);
  cpl_table_delete(aProp->tellregions);
  cpl_free(aProp);
} /* muse_postproc_properties_delete() */

/*----------------------------------------------------------------------------*/
/**
  @brief    Check the --save parameter contents against allowed options.
  @param    aSave    --save parameter string constant
  @param    aValid   comma-delimited string of all valid options
  @return   CPL_TRUE if everything is ok, CPL_FALSE something went wrong.
 */
/*----------------------------------------------------------------------------*/
cpl_boolean
muse_postproc_check_save_param(const char *aSave, const char *aValid)
{
  cpl_ensure(aSave, CPL_ERROR_NULL_INPUT, CPL_FALSE);
  if (strlen(aSave) < 4) { /* less than "cube"! */
    cpl_error_set_message(__func__, CPL_ERROR_DATA_NOT_FOUND,
                          "no (valid) save option given!");
    return CPL_FALSE;
  }
  /* now compare all given output products against the allowed options */
  cpl_boolean allvalid = CPL_TRUE;
  cpl_array *asave = muse_cplarray_new_from_delimited_string(aSave, ","),
            *avalid = muse_cplarray_new_from_delimited_string(aValid, ",");
  int i, j,
      ns = cpl_array_get_size(asave),
      nv = cpl_array_get_size(avalid);
  for (i = 0; i < ns; i++) {
    cpl_boolean valid = CPL_FALSE;
    for (j = 0; j < nv; j++) { /* go through all valid options */
      if (!strcmp(cpl_array_get_string(asave, i),
                  cpl_array_get_string(avalid, j))) {
        valid = CPL_TRUE;
      }
    } /* for j (all valid strings) */
    if (!valid) { /* this string was apparently not valid! */
      cpl_error_set_message(__func__, CPL_ERROR_ILLEGAL_INPUT,
                            "save option %d (%s) is not valid!", i + 1,
                            cpl_array_get_string(asave, i));
      allvalid = CPL_FALSE;
    }
  } /* for i (all given strings) */
  cpl_array_delete(asave);
  cpl_array_delete(avalid);
  return allvalid;
} /* muse_scipost_check_save_param() */

/*----------------------------------------------------------------------------*/
/**
  @brief    Select correct resampling type for resample string.
  @param    aResampleString   the format string from the recipe parameters
  @return   The respective resampling type as muse_resampling_type or
            MUSE_RESAMPLE_WEIGHTED_DRIZZLE on error.

  The framework in principle ensures that no unsupported/unknown resampling
  types arrive here but nevertheless fall back to
  MUSE_RESAMPLE_WEIGHTED_DRIZZLE as the default in case something really weird
  happens.

  @error{set CPL_ERROR_NULL_INPUT\, return MUSE_RESAMPLE_WEIGHTED_DRIZZLE,
         aResampleString is NULL}
 */
/*----------------------------------------------------------------------------*/
muse_resampling_type
muse_postproc_get_resampling_type(const char *aResampleString)
{
  cpl_ensure(aResampleString, CPL_ERROR_NULL_INPUT, MUSE_RESAMPLE_WEIGHTED_DRIZZLE);
  if (!strncmp(aResampleString, "nearest", 8)) {
    return MUSE_RESAMPLE_NEAREST;
  }
  if (!strncmp(aResampleString, "linear", 7)) {
    return MUSE_RESAMPLE_WEIGHTED_LINEAR;
  }
  if (!strncmp(aResampleString, "quadratic", 10)) {
    return MUSE_RESAMPLE_WEIGHTED_QUADRATIC;
  }
  if (!strncmp(aResampleString, "renka", 6)) {
    return MUSE_RESAMPLE_WEIGHTED_RENKA;
  }
  if (!strncmp(aResampleString, "drizzle", 8)) {
    return MUSE_RESAMPLE_WEIGHTED_DRIZZLE;
  }
  if (!strncmp(aResampleString, "lanczos", 8)) {
    return MUSE_RESAMPLE_WEIGHTED_LANCZOS;
  }
  return MUSE_RESAMPLE_WEIGHTED_DRIZZLE; /* catch all fallback */
} /* muse_postproc_get_resampling_type() */

/*----------------------------------------------------------------------------*/
/**
  @brief    Select correct cosmic ray rejection type for crtype string.
  @param    aCRTypeString   the format string from the recipe parameters
  @return   The respective cosmic ray rejection type as
            muse_resampling_crstats_type or MUSE_RESAMPLING_CRSTATS_MEDIAN on
            error.

  The framework in principle ensures that no unsupported/unknown crtypes arrive
  here but nevertheless fall back to MUSE_RESAMPLING_CRSTATS_MEDIAN as the
  default in case something really weird happens.

  @error{set CPL_ERROR_NULL_INPUT\, return MUSE_RESAMPLING_CRSTATS_MEDIAN,
         aCRTypeString is NULL}
 */
/*----------------------------------------------------------------------------*/
muse_resampling_crstats_type
muse_postproc_get_cr_type(const char *aCRTypeString)
{
  cpl_ensure(aCRTypeString, CPL_ERROR_NULL_INPUT, MUSE_RESAMPLING_CRSTATS_IRAF);
  if (!strncmp(aCRTypeString, "iraf", 5)) {
    return MUSE_RESAMPLING_CRSTATS_IRAF;
  }
  if (!strncmp(aCRTypeString, "mean", 5)) {
    return MUSE_RESAMPLING_CRSTATS_MEAN;
  }
  if (!strncmp(aCRTypeString, "median", 7)) {
    return MUSE_RESAMPLING_CRSTATS_MEDIAN;
  }
  return MUSE_RESAMPLING_CRSTATS_MEDIAN; /* catch all fallback */
} /* muse_postproc_get_cr_type() */

/*----------------------------------------------------------------------------*/
/**
  @brief    Select correct cube format for format string.
  @param    aFormatString   the format string from the recipe parameters
  @return   The respective cube format as muse_cube_type or MUSE_CUBE_TYPE_FITS
            on error.

  The framework in principle ensures that no unsupported/unknown formats arrive
  here but nevertheless fall back to MUSE_CUBE_TYPE_FITS as the default in case
  something really weird happens.

  @error{set CPL_ERROR_NULL_INPUT\, return MUSE_CUBE_TYPE_FITS,
         aFormatString is NULL}
 */
/*----------------------------------------------------------------------------*/
muse_cube_type
muse_postproc_get_cube_format(const char *aFormatString)
{
  cpl_ensure(aFormatString, CPL_ERROR_NULL_INPUT, MUSE_CUBE_TYPE_FITS);
  if (!strncmp(aFormatString, "Cube", 5)) {
    return MUSE_CUBE_TYPE_FITS;
  }
  if (!strncmp(aFormatString, "Euro3D", 7)) {
    return MUSE_CUBE_TYPE_EURO3D;
  }
  if (!strncmp(aFormatString, "xCube", 6)) {
    return MUSE_CUBE_TYPE_FITS_X;
  }
  if (!strncmp(aFormatString, "xEuro3D", 8)) {
    return MUSE_CUBE_TYPE_EURO3D_X;
  }
  if (!strncmp(aFormatString, "sdpCube", 8)) {
    return MUSE_CUBE_TYPE_SDP;
  }
  return MUSE_CUBE_TYPE_FITS; /* catch all fallback */
} /* muse_postproc_get_cube_format() */

/*----------------------------------------------------------------------------*/
/**
  @brief    Select correct weighting type for weight string.
  @param    aWeightString   the weight string from the recipe parameters
  @return   The respective weighting type as muse_xcombine_types or
            MUSE_XCOMBINE_EXPTIME on error.

  The framework in principle ensures that no unsupported/unknown weight types arrive
  here but nevertheless fall back to MUSE_XCOMBINE_EXPTIME as the default in case
  something really weird happens.

  @error{set CPL_ERROR_NULL_INPUT\, return MUSE_XCOMBINE_EXPTIME,
         aWeightString is NULL}
 */
/*----------------------------------------------------------------------------*/
muse_xcombine_types
muse_postproc_get_weight_type(const char *aWeightString)
{
  cpl_ensure(aWeightString, CPL_ERROR_NULL_INPUT, MUSE_XCOMBINE_EXPTIME);
  if (!strncmp(aWeightString, "exptime", 8)) {
    return MUSE_XCOMBINE_EXPTIME;
  }
  if (!strncmp(aWeightString, "fwhm", 5)) {
    return MUSE_XCOMBINE_FWHM;
  }
  if (!strncmp(aWeightString, "header", 7)) {
    return MUSE_XCOMBINE_HEADER;
  }
  if (!strncmp(aWeightString, "none", 5)) {
    return MUSE_XCOMBINE_NONE;
  }
  return MUSE_XCOMBINE_EXPTIME; /* catch all fallback */
} /* muse_postproc_get_weight_type() */

/*----------------------------------------------------------------------------*/
/**
  @brief    Load the calibration from a multi-table FITS file that is nearest
            on the sky.
  @param    aHeader      the FITS header with the reference position (RA, DEC)
  @param    aFrame       the frame containing the multi-table file information
  @param    aWarnLimit   minimum distance in arcsec before warning is given
  @param    aErrLimit    minimum distance in arcsec before failing
  @param    aRA          RA of reference source to return (can be NULL)
  @param    aDEC         DEC of reference source to return (can be NULL)
  @return   a table selected to be nearest on the sky or NULL on error

  This loops through all FITS extensions present in the file in aFrame and
  computes the distance on the sky relative to the position taken from aHeader.
  The table from the extension with the nearest distance is then loaded.

  A warning is output if the distance is larger than half the MUSE field size,
  i.e. more than aWarnLimit arcsec.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL, aHeader and/or aFrame are NULL}
  @error{set CPL_ERROR_DATA_NOT_FOUND\, return NULL,
         aHeader does not contain RA and/or DEC}
  @error{set CPL_ERROR_INCOMPATIBLE_INPUT\, return NULL,
         position in aHeader is more than aErrLimit away from all reference sources}
 */
/*----------------------------------------------------------------------------*/
cpl_table *
muse_postproc_load_nearest(const cpl_propertylist *aHeader,
                           const cpl_frame *aFrame, float aWarnLimit,
                           float aErrLimit, double *aRA, double *aDEC)
{
  cpl_ensure(aHeader && aFrame, CPL_ERROR_NULL_INPUT, NULL);
  cpl_errorstate state = cpl_errorstate_get();
  double raref = muse_pfits_get_ra(aHeader),
         decref = muse_pfits_get_dec(aHeader);
  cpl_ensure(cpl_errorstate_is_equal(state), CPL_ERROR_DATA_NOT_FOUND, NULL);
  cpl_msg_debug(__func__, "reference coordinates: RA = %e deg, DEC =%e deg",
                raref, decref);
  if (aRA) {
    *aRA = raref;
  }
  if (aDEC) {
    *aDEC = decref;
  }

  const char *fn = cpl_frame_get_filename(aFrame);
  cpl_propertylist *header;
  double dmin = FLT_MAX; /* minimum distance yet found in deg */
  int iext, inearest = -1, next = cpl_fits_count_extensions(fn);
  for (iext = 1; iext <= next; iext++) {
    header = cpl_propertylist_load(fn, iext);
    const char *extname = cpl_propertylist_get_string(header, "EXTNAME");
    double ra = muse_pfits_get_ra(header),
           dec = muse_pfits_get_dec(header),
           d = muse_astro_angular_distance(ra, dec, raref, decref);
    cpl_msg_debug(__func__, "extension %d [%s]: RA = %e deg, DEC = %e deg --> "
                  "d = %e deg", iext, extname, ra, dec, d);
    if (d < dmin) {
      dmin = d;
      inearest = iext;
    }
    cpl_propertylist_delete(header);
  } /* for iext */
  /* warn for distances larger than x arcsec */
  if (dmin * 3600. > aErrLimit) {
    char *msg = cpl_sprintf("Distance of nearest reference table to observed "
                            "position is %.2f arcmin, certainly a wrong "
                            "reference object!", dmin * 60.);
    cpl_msg_error(__func__, "%s", msg);
    cpl_error_set_message(__func__, CPL_ERROR_INCOMPATIBLE_INPUT, "%s", msg);
    cpl_free(msg);
    return NULL;
  }
  if (dmin * 3600. > aWarnLimit) {
    cpl_msg_warning(__func__, "Distance of nearest reference table to observed "
                    "position is %.2f arcmin! (Wrong reference object?)",
                    dmin * 60.);
  }
  header = cpl_propertylist_load(fn, inearest);
  const char *extname = cpl_propertylist_get_string(header, "EXTNAME");
  cpl_msg_info(__func__, "Loading \"%s[%s]\" (distance %.1f arcsec)", fn,
               extname, dmin * 3600.);
  cpl_propertylist_delete(header);
  cpl_table *table = cpl_table_load(fn, inearest, 1);
  return table;
} /* muse_postproc_load_nearest() */

/*----------------------------------------------------------------------------*/
/**
  @brief    Revert correction of on-sky data with the flat-field spectrum.
  @param    aPt         pixel table with the on-sky data
  @param    aResponse   the response table with header
  @return   CPL_ERROR_NONE if nothing needs to be done or on success, another
            CPL error code on failure.

  This function is a dirty trick to improve backward compatibility: if an old
  (pre v2.0, without flat-field spectrum correction) STD_RESPONSE was passed to
  the one of the post-processing recipes, then make the (merged) pixel table
  compatible to that response, by reverting application of the flat-field
  spectrum.
  In effect, this function divides the pixel table again by the averaged
  flat-field spectrum.
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_postproc_revert_ffspec_maybe(muse_pixtable *aPt,
                                  const muse_table *aResponse)
{
  cpl_ensure_code(aPt && aPt->header, CPL_ERROR_NULL_INPUT);
  if (!aResponse) {
    return CPL_ERROR_NONE;
  }
  /* cross-check the header of the response curve against *
   * the flat-field spectrum status of the pixel table    */
  cpl_boolean respff = cpl_propertylist_has(aResponse->header,
                                            MUSE_HDR_FLUX_FFCORR),
              ptff = cpl_propertylist_has(aPt->header, MUSE_HDR_PT_FFCORR);
  if (respff == ptff || !ptff) {
    /* same state for both means we don't need to do anything, same if *
     * the pixel table was not corrected for ffspec                    */
    return CPL_ERROR_NONE;
  }

  cpl_msg_warning(__func__, "Adapt pixel table to %s for backward compatibility"
                  ": revert correction by flat-field spectrum!",
                  MUSE_TAG_STD_RESPONSE);

  /* so we need to divide again by the flat-field spectrum */
  cpl_array *lambdas = muse_cpltable_extract_column(aPt->ffspec,
                                                    MUSE_PIXTABLE_FFLAMBDA),
            *ffdata = muse_cpltable_extract_column(aPt->ffspec,
                                                   MUSE_PIXTABLE_FFDATA);
  muse_pixtable_spectrum_apply(aPt, lambdas, ffdata,
                               MUSE_PIXTABLE_OPERATION_DIVIDE);
  cpl_array_unwrap(lambdas);
  cpl_array_unwrap(ffdata);
  cpl_table_delete(aPt->ffspec);
  aPt->ffspec = NULL;
  cpl_propertylist_erase(aPt->header, MUSE_HDR_PT_FFCORR);
  cpl_msg_info(__func__, "Pixel table now convolved with flat-field spectrum "
               "again, removed %s keyword from header.", MUSE_HDR_PT_FFCORR);
  return CPL_ERROR_NONE;
} /* muse_postproc_revert_ffspec_maybe() */

/*----------------------------------------------------------------------------*/
/**
  @brief    Create a white-light image of the field of view.
  @param    aPixtable   Pixel table with the exposure.
  @param    aCRSigma    Sigma level for cocmic ray rejection.
  @return   A muse_image * on success or NULL on error.

  If aCRSigma is a positive number, detect cosmic rays, flag them in the pixel
  table, and ignore them in the whitelight image.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL, aPixtable is NULL}
 */
/*----------------------------------------------------------------------------*/
muse_image *
muse_postproc_whitelight(muse_pixtable *aPixtable, double aCRSigma)
{
  cpl_ensure(aPixtable, CPL_ERROR_NULL_INPUT, NULL);

  cpl_boolean usegrid = getenv("MUSE_COLLAPSE_PIXTABLE")
                      && atoi(getenv("MUSE_COLLAPSE_PIXTABLE")) > 0;
  muse_resampling_type type = usegrid ? MUSE_RESAMPLE_NONE
                                      : MUSE_RESAMPLE_WEIGHTED_DRIZZLE;
  muse_resampling_params *params = muse_resampling_params_new(type);
  if (aCRSigma > 0.) {
    params->crtype = MUSE_RESAMPLING_CRSTATS_MEDIAN;
    params->crsigma = aCRSigma;
  }
  muse_pixgrid *grid = NULL;
  muse_datacube *cube = muse_resampling_cube(aPixtable, params,
                                             usegrid ? &grid : NULL);
  if (cube == NULL) {
    cpl_msg_error(__func__, "Could not create cube for whitelight image");
    muse_resampling_params_delete(params);
    muse_pixgrid_delete(grid);
    return NULL;
  }
  muse_image *image = NULL;
  muse_table *fwhite = muse_table_load_filter(NULL, "white");
  if (usegrid) {
    params->method = MUSE_RESAMPLE_WEIGHTED_DRIZZLE;
    image = muse_resampling_collapse_pixgrid(aPixtable, grid,
                                             cube, fwhite, params);
  } else {
    image = muse_datacube_collapse(cube, fwhite);
  }
  muse_resampling_params_delete(params);
  muse_pixgrid_delete(grid);
  muse_datacube_delete(cube);
  muse_table_delete(fwhite);
  return image;
} /* muse_postproc_whitelight() */

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief    Set up and launch the MPDAF-like slice self-calibration.
  @param    aPT        pixel table with the science data.
  @param    aProp      the post-processing properties object
  @param    aOffsets   table with exposure position offsets (optional)
  @param    aACalOut   auto-calibration output structure (optional)
  @param    aCube      output cube (optional)
  @param    aWhite     output white-light image (optional)
  @return   CPL_ERROR_NONE on success, another CPL error code on failure.

  If the input aProp contains a autocal_table component, this is used to
  directly apply the self-calibration and return.

  Otherwise, a sky mask is used to select rows in the input pixel table that are
  then used to compute the self-calibration. This sky mask is either taken as
  the input SKY_MASK (component of aProp). In that case, its header must contain
  a proper world-coordinate system. Or a mask is computed from the data itself,
  by reconstructing and masking the white-light image of the field.

  In the latter case, the optional pointers aCube and aWhite will be filled,
  by the reconstructed cube and white-light image. Otherwise they remain
  untouched.

  @error{set and return CPL_ERROR_NULL_INPUT, aPT and/or aProp are NULL}
  @error{propagate muse_wcs_new() error code,
         a valid WCS could not be constructed from the input sky mask}
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
muse_postproc_run_autocalib(muse_pixtable *aPT, muse_postproc_properties *aProp,
                            const cpl_table *aOffsets,
                            muse_postproc_autocal_outputs *aACalOut,
                            muse_datacube **aCube, muse_image **aWhite)
{
  cpl_ensure_code(aPT && aProp, CPL_ERROR_NULL_INPUT);

  /* test if user autocalib factors table is provided */
  if (aProp->autocal_table) {
    /* yes: use that for self-calibration and return immediately */
    cpl_error_code rc = muse_autocalib_apply(aPT, aProp->autocal_table);
    return rc;
  }

  /* spectra/pixel selection for self-calibration */
  cpl_size nrows = muse_pixtable_get_nrow(aPT);
  if (aProp->sky_mask) {
    cpl_msg_info(__func__, "Preparing masking for self-calibration using input "
                 "mask.");
    cpl_errorstate state = cpl_errorstate_get();
    cpl_table_select_all(aPT->table);
    cpl_size nbefore = cpl_table_count_selected(aPT->table);
    muse_pixtable_and_selected_mask(aPT, aProp->sky_mask, aProp->wcs, aOffsets);
    cpl_size nafter = cpl_table_count_selected(aPT->table);
    if (!cpl_errorstate_is_equal(state)) {
      if (nafter < nbefore) {
        cpl_msg_warning(__func__, "An error occurred during pixel selection "
                        "using %s: %s", MUSE_TAG_SKY_MASK,
                        cpl_error_get_message());
        /* Since some selection happened, this was likely successful, *
         * even though some errors occurred. Swallow the error to not *
         * cause more confusion.                                      */
        cpl_errorstate_set(state);
      } else {
        cpl_msg_error(__func__, "Selecting sky pixels using %s failed: %s",
                      MUSE_TAG_SKY_MASK, cpl_error_get_message());
        return cpl_error_get_code();
      } /* else */
    } /* if error state */
  } else {
    /* Create white light image and sky mask */
    cpl_msg_info(__func__, "Intermediate resampling to produce white-light "
                 "image to create auto calibration mask");
    cpl_msg_indent_more();
    muse_resampling_params *params = muse_resampling_params_new(MUSE_RESAMPLE_WEIGHTED_DRIZZLE);
    params->crtype = MUSE_RESAMPLING_CRSTATS_MEDIAN;
    params->crsigma = aProp->skymodel_params.crsigmac;
    muse_datacube *cube = muse_resampling_cube(aPT, params, NULL);
    muse_resampling_params_delete(params);
    if (!cube) {
      cpl_msg_error(__func__, "Could not create cube for white-light image");
      return CPL_ERROR_ILLEGAL_OUTPUT;
    }
    muse_table *fwhite = muse_table_load_filter(NULL, "white");
    muse_image *whitelight = muse_datacube_collapse(cube, fwhite);
    muse_table_delete(fwhite);
    cpl_msg_indent_less();

    muse_mask *skymask = muse_autocalib_create_mask(whitelight, 5.0,
                /* hardcode only sensible prefix */ "ESO QC SCIPOST");
    cpl_table_select_all(aPT->table);
    muse_pixtable_and_selected_mask(aPT, skymask, NULL, NULL);
    if (aACalOut) {
      aACalOut->mask = skymask;
    } else {
      muse_mask_delete(skymask);
    }
    cpl_size nsel = cpl_table_count_selected(aPT->table);
    cpl_msg_info(__func__, "Mask selected %"CPL_SIZE_FORMAT" pixels (%.2f %%) ",
                 nsel, 100.*nsel/nrows);

    /* save output data, if possible */
    if (aCube) {
      *aCube = cube;
    } else {
      muse_datacube_delete(cube);
    }
    if (aWhite) {
      *aWhite = whitelight;
    } else {
      muse_image_delete(whitelight);
    }
  } /* else: use data itself to create mask */

  if (getenv("MUSE_DEBUG_AUTOCALIB")) {
    cpl_msg_debug(__func__, "Saving pixel table mask");
    nrows = muse_pixtable_get_nrow(aPT);
    cpl_table_new_column(aPT->table, "MASK", CPL_TYPE_INT);
    for (cpl_size n = 0; n < nrows; n++) {
      cpl_table_set_int(aPT->table, "MASK", n,
                        cpl_table_is_selected(aPT->table, n) == 0 ? 1 : 0);
    } /* for all pixel table rows */
  } /* if debug autocalib */

  muse_table **factors = aACalOut ? &(aACalOut->table) : NULL;
  cpl_error_code rc = muse_autocalib_slice_median(aPT, 15.0, factors);
  /* clear the selection in the pixel table before returning */
  cpl_table_select_all(aPT->table);
  return rc;
} /* muse_postproc_run_autocalib() */

/*----------------------------------------------------------------------------*/
/**
  @brief    Set up and run Raman-light removal.
  @param    aPT          pixel table with the science data.
  @param    aProp        the post-processing properties object
  @param    aOffsets     table with exposure position offsets (optional)
  @param    aRamanCube   Raman output structure (datacube with extensions)
  @return   CPL_ERROR_NONE on success, another CPL error code on failure.

  Create mini pixel tables containing only the wavelength range around the two
  main Raman features (of O2 and N2) and create images and sky masks for these.
  Using the sky masks, further remove pixels of artifacts and objects in the
  field, and apply an optional sky mask coming as component of aProp (sky_mask).
  Then also remove bad pixels, these include cosmic rays detected in the
  reconstruction of the two images.

  Then the 2D fit of the Raman signal is carried out, using the information
  of these two cleaned mini pixel tables. The fitted signal is then subtracted
  from the input pixel table (aPT), using the line list (aProp->raman_lines)
  and the instrument LSF (aProp->lsf_cube).

  @error{set and return CPL_ERROR_NULL_INPUT, aPT and/or aProp are NULL}
  @error{output message\, set and return CPL_ERROR_ILLEGAL_INPUT,
         aProp->raman_width less than kMuseSliceSlitWidthA}
  @error{output message\, set and return CPL_ERROR_DATA_NOT_FOUND,
         aProp->raman_lines and/or aProp->lsf_cube are NULL}
  @error{output message\, but return CPL_ERROR_NONE,
         the pixel table does not contain data around either Raman feature}
  @error{output warning\, try to do the correction\, but do not set error code,
         the pixel table has partial coverage}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_postproc_correct_raman(muse_pixtable *aPT, muse_postproc_properties *aProp,
                            const cpl_table *aOffsets,
                            muse_datacube **aRamanCube)
{
  if (aRamanCube) {
    *aRamanCube = NULL; /* NULL output input pointer for the failure cases */
  }
  cpl_ensure_code(aPT && aProp, CPL_ERROR_NULL_INPUT);
  if (aProp->raman_width < kMuseSliceSlitWidthA) {
    cpl_msg_error(__func__, "Raman signal modelling not done, given width "
                  "parameter only %.2f Angstrom (should be %.2f at least)!",
                  aProp->raman_width, kMuseSliceSlitWidthA);
    return cpl_error_set(__func__, CPL_ERROR_ILLEGAL_INPUT);
  }
  if (!aProp->raman_lines) {
    cpl_msg_error(__func__, "Raman signal modelling not done, %s missing!",
                  MUSE_TAG_RAMAN_LINES);
    return cpl_error_set(__func__, CPL_ERROR_DATA_NOT_FOUND);
  }
  if (!aProp->lsf_cube) {
    cpl_msg_error(__func__, "Raman signal modelling not done, %s missing!",
                  MUSE_TAG_LSF_PROFILE);
    return cpl_error_set(__func__, CPL_ERROR_DATA_NOT_FOUND);
  }

  /* central wavelengths of Raman peaks */
  double wl[MUSE_RAMAN_RANGES] = {
    6484., /* O2 */
    6827.  /* N2 */
  };
  float llo = cpl_propertylist_get_float(aPT->header, MUSE_HDR_PT_LLO),
        lhi = cpl_propertylist_get_float(aPT->header, MUSE_HDR_PT_LHI);
  if (llo > wl[1] || lhi < wl[0]) {
    cpl_msg_info(__func__, "Available wavelengths (%.3f ... %.3f) do not cover "
                 "Raman features, not trying to correct them.", llo, lhi);
    return CPL_ERROR_NONE;
  }

  double cputime = cpl_test_get_cputime(),
         walltime = cpl_test_get_walltime();

  /* Adding these columns can take a minute or more, better output something. */
  cpl_msg_debug(__func__, "Adding temporary pixel table columns...");
  /* The temporary "lsf" column holds the combined line spread    *
   * function for the Raman spectrum on both wavelength (6484 and *
   * 6827), including the side lines.                             */
  cpl_msg_debug(__func__, "... lsf ...");
  cpl_table_new_column(aPT->table, "lsf", CPL_TYPE_DOUBLE);
  cpl_table_fill_column_window_double(aPT->table, "lsf", 0,
                                      cpl_table_get_nrow(aPT->table), 0);
  /* The temporary "raman" column holds the simulated Raman *
   * illumination, to be subtracted from the "data" column. */
  cpl_msg_debug(__func__, "... raman ...");
  cpl_table_new_column(aPT->table, "raman", CPL_TYPE_DOUBLE);
  cpl_table_fill_column_window_double(aPT->table, "raman", 0,
                                      cpl_table_get_nrow(aPT->table), 0);
  cpl_msg_debug(__func__, "... done.");

  /* Create mini-pixtables containing just the wavelength range to fit. */
  muse_pixtable *pt[MUSE_RAMAN_RANGES];
  muse_image *images[MUSE_RAMAN_RANGES];
  muse_mask *sky_maskr[MUSE_RAMAN_RANGES];
  int i;
  for (i = 0; i < MUSE_RAMAN_RANGES; i++) {
    double lo = wl[i] - aProp->raman_width * 0.5,
           hi = wl[i] + aProp->raman_width * 0.5;
    pt[i] = muse_pixtable_extract_wavelength(aPT, lo, hi);
    if (!pt[i] || (pt[i] && muse_pixtable_get_nrow(pt[i]) == 0)) {
      cpl_msg_warning(__func__, "Raman correction for %s feature not possible, "
                      "%.3f..%.3f Angstrom is outside the wavelength range!",
                      i == 0 ? "O2" : "N2", lo, hi);
      muse_pixtable_delete(pt[i]);
      pt[i] = NULL;
      images[i] = NULL;
      sky_maskr[i] = NULL;
      continue;
    }

    /* Create sky masks: one for each wavelength range */
    cpl_msg_info(__func__, "Intermediate resampling to produce white-light image"
                 " for Raman correction (%s feature, %.3f..%.3f Angstrom):",
                 i == 0 ? "O2" : "N2", lo, hi);
    cpl_msg_indent_more();
    images[i] = muse_postproc_whitelight(pt[i], aProp->skymodel_params.crsigmac);
    cpl_msg_indent_less();
    if (!images[i]) {
      /* should not occur, since the pixel table is checked above */
      sky_maskr[i] = NULL;
      continue;
    }
    sky_maskr[i] = muse_sky_create_skymask(images[i], aProp->skymodel_params.ignore,
                                           aProp->skymodel_params.fraction,
                                           "ESO QC SCIPOST");
    if (aRamanCube) {
      /* not used a real white filter, and the unit is irrelevant here */
      cpl_propertylist_erase_regexp(images[i]->header, "ESO DRS MUSE", 0);
      cpl_propertylist_update_string(images[i]->header, "BUNIT", "");
      /* the DQ extension is not needed, use NANs instead */
      muse_image_reject_from_dq(images[i]);
      cpl_image_fill_rejected(images[i]->data, NAN);
      cpl_image_delete(images[i]->dq);
      images[i]->dq = NULL;
    } else {
      muse_image_delete(images[i]);
    }
  } /* for i (both Raman ranges) */

  /* check if any one of the two Raman features can be used at all */
  if (!sky_maskr[0] && !sky_maskr[1]) {
    /* no correction is possible, clean up and return */
    cpl_msg_info(__func__, "Neither O2 nor N2 features can be analysed, not "
                 "carrying out Raman correction.");
    for (i = 0; i < MUSE_RAMAN_RANGES; i++) {
      muse_image_delete(images[i]);
      muse_pixtable_delete(pt[i]);
    } /* for i (both Raman ranges) */
    cpl_table_erase_column(aPT->table, "raman");
    cpl_table_erase_column(aPT->table, "lsf");
    return CPL_ERROR_NONE;
  } /* no sky mask */

  /* Remove masked pixels of both ranges (and possibly an input sky mask) *
   * from both mini pixel tables, and then remove all bad pixels as well. */
  for (i = 0; i < MUSE_RAMAN_RANGES; i++) {
    if (!pt[i] || !sky_maskr[i]) {
      continue;
    }
    int j;
    cpl_table_select_all(pt[i]->table);
    for (j = 0; j < MUSE_RAMAN_RANGES; j++) {
      if (!pt[j] || !sky_maskr[j]) {
        continue;
      }
      muse_pixtable_and_selected_mask(pt[i], sky_maskr[j], NULL, NULL);
    }
    /* select pixels of an overall sky mask, possibly using its WCS */
    if (aProp->sky_mask) {
      muse_pixtable_and_selected_mask(pt[i], aProp->sky_mask, aProp->wcs,
                                      aOffsets);
    }
    cpl_table_not_selected(pt[i]->table);
    cpl_table_erase_selected(pt[i]->table);

    /* remove remaining bad pixels, including the ones that were   *
     * marked as cosmic rays during the white-light image creation */
    cpl_table_and_selected_int(pt[i]->table, MUSE_PIXTABLE_DQ,
                               CPL_NOT_EQUAL_TO, EURO3D_GOODPIXEL);
    cpl_table_erase_selected(pt[i]->table);
  } /* for i (both Raman ranges) */

   /* Clean up the masks if that don't get propagated out. */
  for (i = 0; !aRamanCube && i < MUSE_RAMAN_RANGES; i++) {
    muse_mask_delete(sky_maskr[i]);
  } /* for i (both Raman ranges) */

  /* Add the LSF column to each pixel table, and do the fit. */
  for (i = 0; i < MUSE_RAMAN_RANGES; i++) {
    if (!pt[i]) {
      continue;
    }
    muse_raman_add_lsf(pt[i], aProp->raman_lines, i, 1.0, aProp->lsf_cube);
  } /* for i (both Raman ranges) */

  /* Do the 2D fit for the Raman signal, to both wavelength ranges. */
  cpl_array *param = muse_raman_fit(pt);

  /* Now we can clean up both mini pixel tables. */
  for (i = 0; i < MUSE_RAMAN_RANGES; i++) {
    muse_pixtable_delete(pt[i]);
  } /* for i (both Raman ranges) */

  /* Compute the (mean) flux of the two Raman features. */
  /* first get the unit, to see if we have to scale the fluxes */
  const char *colunit = cpl_table_get_column_unit(aProp->raman_lines, "flux");
  char *unit = NULL;
  double fscale = 1.;
  if (colunit) {
    if (strstr(colunit, "10**(-20)") || strstr(colunit, "10^(-20)")) {
      fscale = 1e-20;
      char *p = strstr(colunit, "erg");
      unit = cpl_sprintf("%s", p);
    } else {
      /* seems that there is no scale */
      unit = cpl_sprintf("%s", colunit);
    }
  } /* if colunit */
  double totflux[MUSE_RAMAN_RANGES];
  for (i = 0; i < MUSE_RAMAN_RANGES; i++) {
    /* flux in arbitrary units from the fit */
    totflux[i] = cpl_array_get(param, 5 + i, NULL) * fscale;

    /* multiply by total flux for the feature from the table */
    cpl_table_select_all(aProp->raman_lines);
    cpl_table_and_selected_int(aProp->raman_lines, "group", CPL_EQUAL_TO, i);
    cpl_table *t = cpl_table_extract_selected(aProp->raman_lines);
    double sum = cpl_table_get_nrow(t) * cpl_table_get_column_mean(t, "flux");
    cpl_table_delete(t);
    totflux[i] *= sum;
  } /* for i (both Raman ranges) */
  cpl_table_select_all(aProp->raman_lines);

  /* debug: dump the parameters */
  cpl_msg_info(__func__, "spatial: %g x^2 %+g xy %+g y^2 %+g x %+g y +1",
               cpl_array_get(param, 0, NULL), cpl_array_get(param, 1, NULL),
               cpl_array_get(param, 2, NULL), cpl_array_get(param, 3, NULL),
               cpl_array_get(param, 4, NULL));
  cpl_msg_info(__func__, "O2 flux = %g, N2 flux = %g %s", totflux[0], totflux[1],
               unit);

  if (aRamanCube) {
    /* Reconstruct an image of the Raman light distribution as well. */
    muse_image *fits[MUSE_RAMAN_RANGES];
    for (i = 0; i < MUSE_RAMAN_RANGES; i++) {
      if (!images[i]) {
        fits[i] = NULL;
      } else {
        fits[i] = muse_raman_simulate_image(images[i], param);
      }
    } /* for i (both Raman ranges) */

    /* Now, to be able to saving everything into one file, use a datacube. *
     * As the "cube", use the fitted Raman light distribution in arbitrary *
     * units.                                                              */
    *aRamanCube = cpl_calloc(1, sizeof(muse_datacube));
    (*aRamanCube)->data = cpl_imagelist_new();
    if (fits[0]) {
      cpl_imagelist_set((*aRamanCube)->data, cpl_image_duplicate(fits[0]->data), 0);
      (*aRamanCube)->header = cpl_propertylist_duplicate(fits[0]->header);
    } else {
      cpl_imagelist_set((*aRamanCube)->data, cpl_image_duplicate(fits[1]->data), 0);
      (*aRamanCube)->header = cpl_propertylist_duplicate(fits[1]->header);
    }

    /* write some QC parameters */
    cpl_propertylist_append_double((*aRamanCube)->header, QC_POST_RAMAN_SP_XX,
                                   cpl_array_get(param, 0, NULL));
    cpl_propertylist_append_double((*aRamanCube)->header, QC_POST_RAMAN_SP_XY,
                                   cpl_array_get(param, 1, NULL));
    cpl_propertylist_append_double((*aRamanCube)->header, QC_POST_RAMAN_SP_YY,
                                   cpl_array_get(param, 2, NULL));
    cpl_propertylist_append_double((*aRamanCube)->header, QC_POST_RAMAN_SP_X,
                                   cpl_array_get(param, 3, NULL));
    cpl_propertylist_append_double((*aRamanCube)->header, QC_POST_RAMAN_SP_Y,
                                   cpl_array_get(param, 4, NULL));
    cpl_propertylist_append_float((*aRamanCube)->header, QC_POST_RAMAN_FLUXO2,
                                  totflux[0]);
    cpl_propertylist_append_float((*aRamanCube)->header, QC_POST_RAMAN_FLUXN2,
                                  totflux[1]);

    /* To be able to use the datacube structure for saving, *
     * convert the sky masks into images.                   */
    muse_image *masks[MUSE_RAMAN_RANGES];
    for (i = 0; i < MUSE_RAMAN_RANGES; i++) {
      if (!sky_maskr[i]) {
        masks[i] = NULL;
        continue;
      }
      masks[i] = muse_image_new();
      masks[i]->header = cpl_propertylist_duplicate(sky_maskr[i]->header);
      masks[i]->data = cpl_image_new_from_mask(sky_maskr[i]->mask);
      muse_mask_delete(sky_maskr[i]);

      /* Also scale both Raman images using the flux of the Raman lines. */
      if (!fits[i]) {
        continue;
      }
      cpl_image_multiply_scalar(fits[i]->data, totflux[i]);
      cpl_propertylist_update_string(fits[i]->header, "BUNIT",
                                     kMuseFluxUnitString);
    } /* for i (both Raman ranges) */

    (*aRamanCube)->recimages = muse_imagelist_new();
    (*aRamanCube)->recnames = cpl_array_new(6, CPL_TYPE_STRING);
    cpl_errorstate state = cpl_errorstate_get();
    muse_imagelist_set((*aRamanCube)->recimages, images[0], 0);
    muse_imagelist_set((*aRamanCube)->recimages, masks[0], 1);
    muse_imagelist_set((*aRamanCube)->recimages, images[1], 2);
    muse_imagelist_set((*aRamanCube)->recimages, masks[1], 3);
    muse_imagelist_set((*aRamanCube)->recimages, fits[0], 4);
    muse_imagelist_set((*aRamanCube)->recimages, fits[1], 5);
    if (!cpl_errorstate_is_equal(state)) {
      cpl_msg_debug(__func__, "Some images for the Raman cube were not saved...");
      /* swallow the errors */
      cpl_errorstate_set(state);
    }
    cpl_array_set_string((*aRamanCube)->recnames, 0, "RAMAN_IMAGE_O2");
    cpl_array_set_string((*aRamanCube)->recnames, 1, "SKY_MASK_O2");
    cpl_array_set_string((*aRamanCube)->recnames, 2, "RAMAN_IMAGE_N2");
    cpl_array_set_string((*aRamanCube)->recnames, 3, "SKY_MASK_N2");
    cpl_array_set_string((*aRamanCube)->recnames, 4, "RAMAN_FIT_O2");
    cpl_array_set_string((*aRamanCube)->recnames, 5, "RAMAN_FIT_N2");
  } /* if aRamanCube */
  cpl_free(unit);

  /* Subtract the raman light from the pixel table.                        *
   * This is a bit tricky: since we have only a global simulation function *
   * (independent of the group), but the groups differ by flux, we need to *
   * put the group individual flux into the lsf column.                    */
  for (i = 0; i < MUSE_RAMAN_RANGES; i++) {
    double flux = cpl_array_get(param, 5 + i, NULL);
    muse_raman_add_lsf(aPT, aProp->raman_lines, i, flux, aProp->lsf_cube);
  }
  cpl_msg_info(__func__, "Simulate Raman light with given parameters");
  cpl_array *raman = muse_cpltable_extract_column(aPT->table, "raman");
  muse_raman_simulate(aPT, param, raman);
  cpl_array_delete(param);
  cpl_array_unwrap(raman);
  cpl_msg_info(__func__, "Subtract Raman light");
  cpl_table_subtract_columns(aPT->table, MUSE_PIXTABLE_DATA, "raman");

  /* Clean the extra pixel table columns up again. */
  cpl_table_erase_column(aPT->table, "raman");
  cpl_table_erase_column(aPT->table, "lsf");

  cpl_msg_debug(__func__, "Raman correction took %gs (CPU time), %gs (wall-clock)",
                cpl_test_get_cputime() - cputime,
                cpl_test_get_walltime() - walltime);

  return CPL_ERROR_NONE;
} /* muse_postproc_correct_raman() */

/*----------------------------------------------------------------------------*/
/**
  @brief    Merge and process pixel tables from one exposure.
  @param    aProp        the post-processing properties object
  @param    aIndex       index of the exposure (starting with 0)
  @param    aACalOut     auto-calibration output structure (optional)
  @param    aRamanCube   Raman output structure (datacube, optional)
  @param    aSkyOut      sky output structure (optional)
  @param    aOffsets     table with exposure position offsets (optional)
  @return   a valid pointer representing the object expected by the calling
            recipe or NULL on error

  Depending on processing type requested in the properties object, either apply
  all calibrations (and return a pixel table), or derive the flux response (and
  return a response table plus a telluric correction table), or derive an
  astrometric calibration (and return it).

  On output, the components of the aRamanCube and aSkyOut structures may have
  been filled (depending on the inputs and parameters given in aProp). Then,
  they need to be deallocated with the respective functions.

  @pseudocode
  pixeltable = muse_pixtable_load_merge_channels()
  if not standard:
    muse_flux_calibrate()
  muse_sky_subtract_pixtable(pixeltable)
  if WFM:
    muse_dar_correct()
    muse_dar_check()
  if standard:
    muse_flux_integrate_std()
    muse_flux_response_compute()
    muse_flux_get_response_table()
    muse_flux_get_telluric_table()
    return
  if astrometry:
    muse_wcs_locate_sources()
    muse_wcs_solve()
    return
  muse_wcs_project_tan()@endpseudocode

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         aProp or aProp->exposures are NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         aIndex is larger than the last index in the aProp->exposures table}
  @error{set CPL_ERROR_TYPE_MISMATCH but return valid object,
         the pixel table is in an unknown state of astrometric calibration}
 */
/*----------------------------------------------------------------------------*/
void *
muse_postproc_process_exposure(muse_postproc_properties *aProp,
                               unsigned int aIndex,
                               muse_postproc_autocal_outputs *aACalOut,
                               muse_datacube **aRamanCube,
                               muse_postproc_sky_outputs *aSkyOut,
                               const cpl_table *aOffsets)
{
  cpl_ensure(aProp && aProp->exposures, CPL_ERROR_NULL_INPUT, NULL);
  cpl_ensure((int)aIndex < cpl_table_get_nrow(aProp->exposures),
             CPL_ERROR_ILLEGAL_INPUT, NULL);
  cpl_msg_info(__func__, "Starting to process exposure %d (DATE-OBS=%s)",
               aIndex + 1, cpl_table_get_string(aProp->exposures, "DATE-OBS",
                                                aIndex));
  cpl_ensure(aProp->exposures, CPL_ERROR_NULL_INPUT, NULL);

  cpl_table *exposure = cpl_table_extract(aProp->exposures, aIndex, 1);
  muse_pixtable *pt = muse_pixtable_load_merge_channels(exposure,
                                                        aProp->lambdamin,
                                                        aProp->lambdamax);
  cpl_table_delete(exposure);
  if (!pt) {
    return NULL;
  }
  /* erase pre-existing QC parameters */
  cpl_propertylist_erase_regexp(pt->header, "ESO QC ", 0);

  /* Override RA, DEC and DROT POSANG if required for the superflat */
  if (getenv("MUSE_SUPERFLAT_POS")) {
    cpl_array *values = muse_cplarray_new_from_delimited_string(
        getenv("MUSE_SUPERFLAT_POS"), ",");
    int nval = cpl_array_get_size(values);
    if (nval != 3) {
      cpl_msg_error(__func__, "MUSE_SUPERFLAT_POS needs 3 parameters, only"
                      "%d were found.", nval);
      cpl_array_delete(values);
    }
    double ra = atof(cpl_array_get_string(values, 0));
    cpl_propertylist_set_double(pt->header, "RA", ra);
    double dec = atof(cpl_array_get_string(values, 1));
    cpl_propertylist_set_double(pt->header, "DEC", dec);
    double drot = atof(cpl_array_get_string(values, 2));
    cpl_propertylist_set_double(pt->header, "ESO INS DROT POSANG", drot);
    cpl_msg_info(__func__, "Overriding RA=%f, DEC=%f, DROT=%f", ra, dec, drot);
    cpl_array_delete(values);
  } /* if superflat positioning */

  /* see if we need to revert the flat-field spectrum correction, *
   * to match pixel table and response curve                      */
  muse_postproc_revert_ffspec_maybe(pt, aProp->response);

  /* atmospheric refraction correction */
  cpl_boolean labdata = CPL_FALSE;
  cpl_errorstate prestate = cpl_errorstate_get();
  double airmass = muse_astro_airmass(pt->header);
  double ra = muse_pfits_get_ra(pt->header),
         dec = muse_pfits_get_dec(pt->header);
  /* try to recognize a lab exposure by missing RA/DEC or weird airmass */
  if (!cpl_errorstate_is_equal(prestate) || airmass < 1.) {
    cpl_msg_warning(__func__, "This seems to be lab data (RA=%f DEC=%f, "
                    "airmass=%f), not doing any DAR correction!", ra, dec,
                    airmass);
    cpl_errorstate_dump(prestate, CPL_FALSE, NULL);
    cpl_errorstate_set(prestate);
    labdata = CPL_TRUE;
  }
  cpl_error_code rc = CPL_ERROR_NONE;
  if (!labdata && muse_pfits_get_mode(pt->header) <= MUSE_MODE_WFM_AO_N) {
    cpl_msg_debug(__func__, "WFM detected: starting DAR correction");
    rc = muse_dar_correct(pt, aProp->lambdaref);
    cpl_msg_debug(__func__, "DAR correction returned rc=%d: %s", rc,
                  rc != CPL_ERROR_NONE ? cpl_error_get_message() : "");
  } /* if not labdata but WFM */
  /* check and possibly correct the DAR quality, if requested to do so */
  if (aProp->darcheck != MUSE_POSTPROC_DARCHECK_NONE) {
    cpl_boolean dorefine = aProp->darcheck == MUSE_POSTPROC_DARCHECK_CORRECT;
    double maxshift = 0;
    rc = muse_dar_check(pt, &maxshift, dorefine, NULL);
    if (rc != CPL_ERROR_NONE) {
      cpl_msg_warning(__func__, "Maximum %s shift %f\'\' (rc = %d: %s)",
                      dorefine ? "corrected" : "detected", maxshift, rc,
                      cpl_error_get_message());
    } else {
      cpl_msg_info(__func__, "Maximum %s shift %f\'\'",
                   dorefine ? "corrected" : "detected", maxshift);
    }
  } /* if DAR checking */

  /* do flux calibration (before sky subtraction!), if possible */
  if (aProp->type != MUSE_POSTPROC_STANDARD) {
    if (aProp->response) {
      rc = muse_flux_calibrate(pt, aProp->response, aProp->extinction,
                               aProp->telluric);
      if (rc != CPL_ERROR_NONE && rc != CPL_ERROR_CONTINUE) {
        /* for a more serious error, output a warning at least *
         * (the data may still be usable)                      */
        cpl_msg_warning(__func__, "Flux calibration returned rc=%d: %s", rc,
                        cpl_error_get_message());
      } else {
        /* for no error or "continue" (an already flux calibrated *
         * table!) a debug message seems more appropriate         */
        cpl_msg_debug(__func__, "Flux calibration returned rc=%d: %s", rc,
                      rc != CPL_ERROR_NONE ? cpl_error_get_message_default(rc) : "");
      }
    } else {
      cpl_msg_info(__func__, "Skipping flux calibration (no %s curve)",
                   MUSE_TAG_STD_RESPONSE);
    } /* else */
  } /* if not POSTPROC_STANDARD */

  /* Raman line correction for AO modes */
  if (muse_pfits_get_mode(pt->header) >= MUSE_MODE_WFM_AO_E &&
      aProp->raman_lines) {
    muse_postproc_correct_raman(pt, aProp, aOffsets, aRamanCube);
  }

  /* prepare objects for cube and white-light image, in case these *
   * are created already for the self-calibration procedure        */
  muse_datacube *cube = NULL;
  muse_image *whitelight = NULL;

  /* run self calibration using partitioned per-slice sky spectra */
  if (aProp->autocalib != MUSE_POSTPROC_AUTOCALIB_NONE) {
    muse_postproc_run_autocalib(pt, aProp, aOffsets, aACalOut, &cube,
                                &whitelight);
  } /* if autocalib */

  /* prepare sky (lines and) continuum for sky subtraction */
  cpl_table *sky_cont = NULL,
            *sky_lines = NULL;
  if ((aProp->skymethod == MUSE_POSTPROC_SKYMETHOD_MODEL && aProp->sky_lines) ||
      (aProp->skymethod == MUSE_POSTPROC_SKYMETHOD_SIMPLE)) {
    /* Process optional sky mask */
    muse_pixtable *sky_pt = muse_pixtable_duplicate(pt);
    if (aProp->sky_mask) {
      cpl_table_select_all(sky_pt->table);
      muse_pixtable_and_selected_mask(sky_pt, aProp->sky_mask, aProp->wcs, aOffsets);
    }
    /* Create white light image and sky mask, if still needed */
    if (!whitelight) {
      if (!cube) {
        cpl_msg_info(__func__, "Intermediate resampling to produce white-light image"
                     " for sky-spectrum creation:");
        cpl_msg_indent_more();
        muse_resampling_params *params = muse_resampling_params_new(MUSE_RESAMPLE_WEIGHTED_DRIZZLE);
        params->crtype = MUSE_RESAMPLING_CRSTATS_MEDIAN;
        params->crsigma = aProp->skymodel_params.crsigmac;
        cube = muse_resampling_cube(sky_pt, params, NULL);
        muse_resampling_params_delete(params);
        if (!cube) {
          cpl_msg_error(__func__, "Could not create cube for whitelight image");
          muse_pixtable_delete(sky_pt);
          muse_pixtable_delete(pt);
          return NULL;
        } /* if still no cube */
      } /* if no cube */
      muse_table *fwhite = muse_table_load_filter(NULL, "white");
      whitelight = muse_datacube_collapse(cube, fwhite);
      muse_table_delete(fwhite);
      cpl_msg_indent_less();
    } /* if no whitelight image */
    muse_mask *sky_mask = muse_sky_create_skymask(whitelight,
                                                  aProp->skymodel_params.ignore,
                                                  aProp->skymodel_params.fraction,
              /* hardcode only sensible prefix */ "ESO QC SCIPOST");

    /* Apply mask and create spectrum, possibly with one CR-reject iteration */
    cpl_table_select_all(sky_pt->table);
    muse_pixtable_and_selected_mask(sky_pt, sky_mask, aProp->wcs, aOffsets);
    cpl_table *spectrum =
      muse_resampling_spectrum_iterate(sky_pt, aProp->skymodel_params.sampling,
                                       0., aProp->skymodel_params.crsigmas, 1);
    if (aProp->skymethod == MUSE_POSTPROC_SKYMETHOD_MODEL) {
      /* Fit sky line spectrum */
      cpl_array *lambda = muse_cpltable_extract_column(spectrum, "lambda");
      double lambda_low = cpl_array_get_min(lambda),
             lambda_high = cpl_array_get_max(lambda);
      sky_lines = cpl_table_duplicate(aProp->sky_lines);
      muse_sky_lines_set_range(sky_lines, lambda_low-5, lambda_high+5);

      /* do the fit, ignoring possible errors */
      prestate = cpl_errorstate_get();
      if (aProp->lsf_cube) {
        cpl_image *lsfImage = muse_lsf_average_cube_all(aProp->lsf_cube, sky_pt);
        muse_wcs *lsfWCS = muse_lsf_cube_get_wcs_all(aProp->lsf_cube);
        /* Filter the image with a rectangle to model the spectrum binning */
        muse_lsf_fold_rectangle(lsfImage, lsfWCS, aProp->skymodel_params.sampling);

        muse_sky_lines_fit(spectrum, sky_lines, lsfImage, lsfWCS);
        if (!cpl_errorstate_is_equal(prestate)) {
          cpl_errorstate_dump(prestate, CPL_FALSE, NULL);
          cpl_errorstate_set(prestate);
        }

        /* Create continuum */
        if (!aProp->sky_continuum) {
          cpl_msg_info(__func__, "No sky continuum given, create it from the data");
          sky_cont = muse_sky_continuum_create(spectrum, sky_lines, lsfImage, lsfWCS,
                                               aProp->skymodel_params.csampling);
        } else {
          cpl_msg_info(__func__, "Using sky continuum given as input");
        }

        /* clean up */
        cpl_image_delete(lsfImage);
#ifdef USE_LSF_PARAMS
      } else if (aProp->lsf_params) { /* Old LSF params code */
        /* do the fit, ignoring possible errors */
        muse_sky_lines_fit_old(spectrum, sky_lines);
        if (!cpl_errorstate_is_equal(prestate)) {
          cpl_errorstate_dump(prestate, CPL_FALSE, NULL);
          cpl_errorstate_set(prestate);
        }

        if (!aProp->sky_continuum) {
          cpl_msg_info(__func__, "No sky continuum given, create it from the data");
          muse_sky_subtract_lines_old(sky_pt, sky_lines, aProp->lsf_params);
          sky_cont = muse_resampling_spectrum(sky_pt,
                                              aProp->skymodel_params.csampling);
          cpl_table_erase_column(sky_cont, "stat");
          cpl_table_erase_column(sky_cont, "dq");
          cpl_table_name_column(sky_cont, "data", "flux");
        }
#endif
      }
      cpl_array_unwrap(lambda);
    } else {
      /* simple subtraction: pretend that the spectrum is a     *
       * continuum and use that function for direct subtraction */
      cpl_table *spec2 = cpl_table_duplicate(spectrum);
      cpl_table_name_column(spec2, "data", "flux");
      rc = muse_sky_subtract_continuum(pt, spec2);
      cpl_table_delete(spec2);
    } /* else: simple */
    if (aSkyOut) {
      aSkyOut->mask = sky_mask;
      aSkyOut->spectrum = spectrum;
    } else {
      muse_mask_delete(sky_mask);
      cpl_table_delete(spectrum);
    }
    muse_pixtable_delete(sky_pt);
  } /* if MUSE_POSTPROC_SKYMETHOD_MODEL or MUSE_POSTPROC_SKYMETHOD_SIMPLE */
  if (aSkyOut) {
    aSkyOut->image = whitelight;
  } else {
    muse_image_delete(whitelight);
  }
  muse_datacube_delete(cube);

  /* In the following we do the sky subtraction, if we have the products *
   * for it. The SKYMETHOD_NONE here reflects the subtract-model option  *
   * of the muse_scipost recipe.                                         */
  if ((aProp->skymethod == MUSE_POSTPROC_SKYMETHOD_NONE) ||
      (aProp->skymethod == MUSE_POSTPROC_SKYMETHOD_MODEL)) {
    if (aProp->sky_continuum) {
      cpl_msg_debug(__func__, "doing sky continuum subtraction with input "
                    "spectrum");
      rc = muse_sky_subtract_continuum(pt, aProp->sky_continuum);
    } else if (sky_cont) {
      cpl_msg_debug(__func__, "doing sky continuum subtraction with created "
                    "spectrum");
      rc = muse_sky_subtract_continuum(pt, sky_cont);
    }
    if (sky_lines) {
      cpl_msg_debug(__func__, "doing sky line subtraction with created list");
      if (aProp->lsf_cube != NULL) {
        rc = muse_sky_subtract_lines(pt, sky_lines, aProp->lsf_cube);
#ifdef USE_LSF_PARAMS
      } else if (aProp->lsf_params != NULL) {
        rc = muse_sky_subtract_lines_old(pt, sky_lines, aProp->lsf_params);
#endif
     }
    } else if (aProp->sky_lines) {
      cpl_msg_debug(__func__, "doing sky line subtraction with input list");
      if (aProp->lsf_cube != NULL) {
        rc = muse_sky_subtract_lines(pt, aProp->sky_lines, aProp->lsf_cube);
#ifdef USE_LSF_PARAMS
      } else if (aProp->lsf_params != NULL) {
        rc = muse_sky_subtract_lines_old(pt, aProp->sky_lines, aProp->lsf_params);
#endif
      }
    }
  } /* if sky modeling */
  /* either transfer the sky lines and continuum out or clean *
   * them up; both should still work, if one or both are NULL */
  if (aSkyOut) {
    aSkyOut->lines = sky_lines;
    aSkyOut->continuum = sky_cont;
  } else {
    cpl_table_delete(sky_lines);
    cpl_table_delete(sky_cont);
  }

  /* carry out radial velocity correction, if specified */
  muse_rvcorrect(pt, aProp->rvtype);

  /* do flux response computation and return */
  if (aProp->type == MUSE_POSTPROC_STANDARD) {
    double raref, decref;
    cpl_table *reftable = muse_postproc_load_nearest(pt->header,
                                                     aProp->refframe, 20., 60.,
                                                     &raref, &decref);
    if (!reftable) {
      cpl_msg_error(__func__, "Correct %s could not be loaded, observed "
                    "target not listed?", MUSE_TAG_STD_FLUX_TABLE);
      muse_pixtable_delete(pt);
      return NULL;
    }
    if (muse_flux_reference_table_check(reftable) != CPL_ERROR_NONE) {
      cpl_msg_error(__func__, "%s does not have a recognized format!",
                    MUSE_TAG_STD_FLUX_TABLE);
      cpl_table_delete(reftable);
      muse_pixtable_delete(pt);
      return NULL;
    }

    prestate = cpl_errorstate_get();
    muse_flux_object *flux = muse_flux_object_new();
    rc = muse_flux_integrate_std(pt, aProp->profile, flux);
    if (flux->intimage) {
      cpl_msg_debug(__func__, "Flux integration found %"CPL_SIZE_FORMAT" stars",
                    cpl_image_get_size_y(flux->intimage->data));
    }
    muse_pixtable_delete(pt);
    if (airmass < 0) {
      cpl_msg_warning(__func__, "Invalid airmass derived (%.3e), resetting to "
                      " zero", airmass);
      airmass = 0.; /* set to zero to leave out uncertain extinction term */
    }
    flux->raref = raref;
    flux->decref = decref;
    rc = muse_flux_response_compute(flux, aProp->select, airmass, reftable,
                                    aProp->tellregions, aProp->extinction);
    cpl_table_delete(reftable);
    rc = muse_flux_get_response_table(flux, aProp->smooth);
    rc = muse_flux_get_telluric_table(flux);
    rc = muse_flux_compute_qc(flux);

    if (!cpl_errorstate_is_equal(prestate)) {
      cpl_msg_warning(__func__, "The following errors happened while computing "
                      "the flux calibration (rc = %d/%s):", rc,
                      cpl_error_get_message_default(rc));
      cpl_errorstate_dump(prestate, CPL_FALSE, muse_cplerrorstate_dump_some);
    }
    return flux;
  } /* if MUSE_POSTPROC_STANDARD */

  /* do astrometry computation and return */
  if (aProp->type == MUSE_POSTPROC_ASTROMETRY) {
    cpl_table *reftable = muse_postproc_load_nearest(pt->header,
                                                     aProp->refframe, 20., 60.,
                                                     NULL, NULL);
    if (!reftable) {
      cpl_msg_error(__func__, "Correct %s could not be loaded, observed "
                    "target not listed?", MUSE_TAG_ASTROMETRY_REFERENCE);
      muse_pixtable_delete(pt);
      return NULL;
    }
    prestate = cpl_errorstate_get();

    muse_wcs_object *wcs = muse_wcs_object_new();
    wcs->crpix1 = aProp->crpix1; /* set center of rotation */
    wcs->crpix2 = aProp->crpix2;
    rc = muse_wcs_locate_sources(pt, fabsf((float)aProp->detsigma),
                                 aProp->centroid, wcs);
    if (aProp->detsigma < 0.) {
      /* loop through possible detection sigmas to find the optimal solution */
      rc = muse_wcs_optimize_solution(wcs, aProp->detsigma, aProp->centroid,
                                      reftable, aProp->radius, aProp->faccuracy,
                                      aProp->niter, aProp->rejsigma);
    } else {
      /* use just the given detection sigma for the solution */
      rc = muse_wcs_solve(wcs, reftable, aProp->radius, aProp->faccuracy,
                          aProp->niter, aProp->rejsigma);
      if (wcs->wcs) { /* set the keyword that muse_wcs_solve() does not save */
        cpl_propertylist_update_float(wcs->wcs, MUSE_HDR_WCS_DETSIGMA,
                                      aProp->detsigma);
        cpl_propertylist_set_comment(wcs->wcs, MUSE_HDR_WCS_DETSIGMA,
                                     MUSE_HDR_WCS_DETSIGMA_C_ONE);
      }
    }
    cpl_table_delete(reftable);
    cpl_msg_debug(__func__, "Solving astrometry (initial radius %.1f pix, "
                  "initial relative accuracy %.1f, %d iterations with rejection"
                  " sigma %.2f) returned rc=%d: %s", aProp->radius,
                  aProp->faccuracy, aProp->niter, aProp->rejsigma,
                  rc, rc != CPL_ERROR_NONE ? cpl_error_get_message() : "");
    muse_pixtable_delete(pt);

    if (!cpl_errorstate_is_equal(prestate)) {
      cpl_msg_warning(__func__, "The following errors happened while computing "
                      "the astrometric solution (rc = %d/%s):", rc,
                      cpl_error_get_message());
      cpl_errorstate_dump(prestate, CPL_FALSE, muse_cplerrorstate_dump_some);
    }
    /* copy solution into the detection image */
    if (wcs->wcs && wcs->cube && wcs->cube->recimages) {
      /* image 1 is the detection image, see muse_wcs_locate_sources() */
      muse_image *mimage = muse_imagelist_get(wcs->cube->recimages, 1);
      if (mimage) {
        muse_wcs_copy_keywords(wcs->wcs, mimage->header, 'S', "Solution");
      }
    } /* if WCS and cube */

    return wcs;
  } /* if MUSE_POSTPROC_ASTROMETRY */

  if (aProp->astrometry) {
    if (muse_pixtable_wcs_check(pt) == MUSE_PIXTABLE_WCS_PIXEL) {
      cpl_propertylist *wcs = aProp->wcs;
      if (!wcs) {
        /* create WCS, trying to find spaxel scale for the instrument mode */
        const char *mode = muse_pfits_get_insmode(pt->header);
        cpl_msg_warning(__func__, "Using default MUSE %cFM astrometry, output world"
                        " coordinates will be inaccurate!", mode[0]);
        wcs = muse_wcs_create_default(pt->header);
      }
      rc = muse_wcs_project_tan(pt, wcs);
      cpl_msg_debug(__func__, "Astrometry correction returned rc=%d: %s", rc,
                    rc != CPL_ERROR_NONE ? cpl_error_get_message() : "");
      if (!aProp->wcs) { /* if this was not input, we need to free again */
        cpl_propertylist_delete(wcs);
      }
    } else if (muse_pixtable_wcs_check(pt) == MUSE_PIXTABLE_WCS_NATSPH) {
      cpl_msg_info(__func__, "Pixel table already projected to TAN, skipping "
                   "astrometric calibration...");
    } else if (muse_pixtable_wcs_check(pt) == MUSE_PIXTABLE_WCS_CELSPH) {
      cpl_msg_info(__func__, "Pixel table already positioned, skipping "
                   "astrometric calibration...");
    } else {
      const char *msg = "Pixel table in unknown state of astrometric calibration!";
      cpl_msg_error(__func__, "%s", msg);
      cpl_error_set_message(__func__, CPL_ERROR_TYPE_MISMATCH, "%s", msg);
    }
  } /* if astrometry */

  return pt;
} /* muse_postproc_process_exposure() */

/*----------------------------------------------------------------------------*/
/**
  @brief    Apply spatial offsets and flux scale to a pixel table (of a single
            exposure)
  @param    aPT        the pixel table to be adapted
  @param    aOffsets   the table with offsets and scalings to apply
  @param    aPurpose   some words about the purpose, just for info-level output

  Their interpretation is copied from muse_xcombine_tables(), but only the first
  value in these variables is interpreted as offset (in deg) and flux scaling
  factor.
 */
/*----------------------------------------------------------------------------*/
void
muse_postproc_offsets_scale(muse_pixtable *aPT, const cpl_table *aOffsets,
                            const char *aPurpose)
{
  if (!aPT || !aOffsets) {
    return;
  }
  if (!aPT->header) {
    return;
  }
  cpl_msg_info(__func__, "Applying offsets to %s...", aPurpose);
  cpl_msg_indent_more();
  double ra =  muse_pfits_get_ra(aPT->header),
         dec = muse_pfits_get_dec(aPT->header),
         *offsets = muse_xcombine_find_offsets(aOffsets,
                                               muse_pfits_get_dateobs(aPT->header));
  char keyword[KEYWORD_LENGTH], comment[KEYWORD_LENGTH];
  if (offsets) {
    if (isfinite(offsets[0]) && isfinite(offsets[1])) {
      ra -= offsets[0];
      dec -= offsets[1];
      cpl_msg_debug(__func__, "Applying coordinate offsets to exposure: %e/%e"
                    " deg", offsets[0], offsets[1]);
      /* update RA/DEC in the header of the pixel table */
      cpl_errorstate state = cpl_errorstate_get();
      cpl_propertylist_update_double(aPT->header, "RA", ra);
      if (!cpl_errorstate_is_equal(state)) {
        cpl_errorstate_set(state);
        cpl_propertylist_update_float(aPT->header, "RA", ra);
      }
      cpl_propertylist_set_comment(aPT->header, "RA", "offset applied!");
      state = cpl_errorstate_get();
      cpl_propertylist_update_double(aPT->header, "DEC", dec);
      if (!cpl_errorstate_is_equal(state)) {
        cpl_errorstate_set(state);
        cpl_propertylist_update_float(aPT->header, "DEC", dec);
      }
      cpl_propertylist_set_comment(aPT->header, "DEC", "offset applied!");
      /* store in the header of the pixel table */
      snprintf(keyword, KEYWORD_LENGTH, MUSE_HDR_OFFSETi_DRA, 1U);
      snprintf(comment, KEYWORD_LENGTH, MUSE_HDR_OFFSETi_DRA_C, offsets[0] * 3600.);
      cpl_propertylist_append_double(aPT->header, keyword, offsets[0]);
      cpl_propertylist_set_comment(aPT->header, keyword, comment);
      snprintf(keyword, KEYWORD_LENGTH, MUSE_HDR_OFFSETi_DDEC, 1U);
      snprintf(comment, KEYWORD_LENGTH, MUSE_HDR_OFFSETi_DDEC_C, offsets[1] * 3600.);
      cpl_propertylist_append_double(aPT->header, keyword, offsets[1]);
      cpl_propertylist_set_comment(aPT->header, keyword, comment);
    } /* if spatial offsets */
    if (isnormal(offsets[2])) { /* valid flux scale */
      /* get and apply the scale */
      cpl_msg_debug(__func__, "Scaling flux of exposure by %g.", offsets[2]);
      muse_pixtable_flux_multiply(aPT, offsets[2]);
      /* store in the header of the pixel table */
      snprintf(keyword, KEYWORD_LENGTH, MUSE_HDR_FLUX_SCALEi, 1U);
      cpl_propertylist_append_double(aPT->header, keyword, offsets[2]);
      cpl_propertylist_set_comment(aPT->header, keyword, MUSE_HDR_FLUX_SCALEi_C);
    } /* if scale */
    /* store date of changed exposure in the header of the output pixel table */
    snprintf(keyword, KEYWORD_LENGTH, MUSE_HDR_OFFSETi_DATEOBS, 1U);
    snprintf(comment, KEYWORD_LENGTH, MUSE_HDR_OFFSETi_DATEOBS_C, 1U);
    cpl_propertylist_append_string(aPT->header, keyword,
                                   muse_pfits_get_dateobs(aPT->header));
    cpl_propertylist_set_comment(aPT->header, keyword, comment);
  } /* if offsets */
  cpl_free(offsets);
  cpl_msg_indent_less();
} /* muse_postproc_offsets_scale() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Find a file with a usable output WCS in the input frameset.
  @param   aProcessing   the processing structure
  @return  A cpl_propertylist with the WCS or NULL on failure

  The file to search should be tagged as MUSE_TAG_OUTPUT_WCS in the input
  aProcessing->inFrames. This function then takes the header of the first
  extension that has NAXIS=2 or 3 (or WCSAXES=2 or 3) and supported CTYPEi
  entries for all relevant axes (supported are RA---TAN for axis1, DEC--TAN for
  axis 2, and AWAV and AWAV-LOG for axis 3).
  If aProcessing->inFrames contains multiple files with the required tag, only
  the first one is taken into account.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL, aProcessing is NULL}
  @error{set CPL_ERROR_NONE\, return NULL,
         no file tagged MUSE_TAG_OUTPUT_WCS was found}
 */
/*---------------------------------------------------------------------------*/
cpl_propertylist *
muse_postproc_cube_load_output_wcs(muse_processing *aProcessing)
{
  cpl_ensure(aProcessing, CPL_ERROR_NULL_INPUT, NULL);

  cpl_frameset *fwcs = muse_frameset_find(aProcessing->inframes,
                                          MUSE_TAG_OUTPUT_WCS, 0, CPL_FALSE);
  if (!fwcs || cpl_frameset_get_size(fwcs) <= 0) {
    /* not an error, quietly return NULL */
    cpl_frameset_delete(fwcs);
    return NULL;
  }
  cpl_frame *frame = cpl_frameset_get_position(fwcs, 0);
  const char *fn = cpl_frame_get_filename(frame);
  cpl_propertylist *header = NULL;
  int iext, next = cpl_fits_count_extensions(fn);
  for (iext = 0; iext <= next; iext++) {
    header = cpl_propertylist_load(fn, iext);
    cpl_errorstate es = cpl_errorstate_get();
    cpl_wcs *wcs = cpl_wcs_new_from_propertylist(header);
    if (!cpl_errorstate_is_equal(es)) {
      cpl_errorstate_set(es);
    }
    if (!wcs) {
      cpl_propertylist_delete(header);
      header = NULL;
      continue; /* try the next extension */
    }
    /* need 2D or 3D WCS to have something useful */
    int naxis = cpl_wcs_get_image_naxis(wcs);
    cpl_boolean valid = naxis == 2 || naxis == 3;
    /* need RA---TAN and DEC--TAN for axes 1 and 2 */
    const cpl_array *ctypes = cpl_wcs_get_ctype(wcs);
    if (valid && cpl_array_get_string(ctypes, 0)) {
      valid = valid && !strncmp(cpl_array_get_string(ctypes, 0), "RA---TAN", 9);
    }
    if (valid && cpl_array_get_string(ctypes, 1)) {
      valid = valid && !strncmp(cpl_array_get_string(ctypes, 1), "DEC--TAN", 9);
    }
    /* need AWAV or AWAV-LOG for axis 3 */
    if (valid && naxis == 3 && cpl_array_get_string(ctypes, 2)) {
      const char *ctype3 = cpl_array_get_string(ctypes, 2);
      valid = valid
            && (!strncmp(ctype3, "AWAV", 5) || !strncmp(ctype3, "AWAV-LOG", 9) ||
                !strncmp(ctype3, "WAVE", 5) || !strncmp(ctype3, "WAVE-LOG", 9));
    }
    if (valid) { /* check for unsupported PCi_j keywords */
      /* still needs to be done on the input header not on the derives WCS! */
      cpl_propertylist *pc = cpl_propertylist_new();
      cpl_propertylist_copy_property_regexp(pc, header, "^PC[0-9]+_[0-9]+", 0);
      valid = cpl_propertylist_get_size(pc) == 0;
      cpl_propertylist_delete(pc);
    }
    cpl_wcs_delete(wcs);
    if (valid) {
      cpl_msg_debug(__func__, "Apparently valid header %s found in extension %d"
                    " of \"%s\"", MUSE_TAG_OUTPUT_WCS, iext, fn);
      muse_processing_append_used(aProcessing, frame, CPL_FRAME_GROUP_CALIB, 1);
      break;
    }
    cpl_propertylist_delete(header);
    header = NULL;
  } /* for iext (all extensions) */
  if (!header) {
    cpl_msg_warning(__func__, "No valid headers for %s found in \"%s\"",
                    MUSE_TAG_OUTPUT_WCS, fn);
  }
  cpl_frameset_delete(fwcs);
  return header;
} /* muse_postproc_cube_load_output_wcs() */

/*---------------------------------------------------------------------------*/
/**
  @brief   High level function to resample to a datacube and collapse that
           to an image of the field of view and save both objects.
  @param   aProcessing    the processing structure
  @param   aPixtable      the MUSE pixel table to resample
  @param   aFormat        the output format of the datacube
  @param   aParams        the structure of resampling parameters
  @param   aFilter        the string with comma-separated filter names to load
  @return  CPL_ERROR_NONE on success or a cpl_error_code on failure

  @error{return CPL_ERROR_NULL_INPUT,
         aProcessing\, aPixtable\, or aParams are NULL}
  @error{return CPL_ERROR_ILLEGAL_INPUT, aFormat contains an invalid format ID}
  @error{propagate error code of muse_resampling_<format>,
         resampling to cube fails}
  @error{propagate error code of muse_processing_save_cube(), saving cube fails}
 */
/*---------------------------------------------------------------------------*/
cpl_error_code
muse_postproc_cube_resample_and_collapse(muse_processing *aProcessing,
                                         muse_pixtable *aPixtable,
                                         muse_cube_type aFormat,
                                         muse_resampling_params *aParams,
                                         const char *aFilter)
{
  cpl_ensure_code(aProcessing && aPixtable && aParams, CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(aFormat == MUSE_CUBE_TYPE_EURO3D ||
                  aFormat == MUSE_CUBE_TYPE_FITS ||
                  aFormat == MUSE_CUBE_TYPE_EURO3D_X ||
                  aFormat == MUSE_CUBE_TYPE_FITS_X ||
                  aFormat == MUSE_CUBE_TYPE_SDP, CPL_ERROR_ILLEGAL_INPUT);

  /* convert cube from air to vacuum wavelengths, if needed */
  if (aParams->tlambda == MUSE_RESAMPLING_DISP_WAVE ||
      aParams->tlambda == MUSE_RESAMPLING_DISP_WAVE_LOG) {
    /* convert using standard air (and Ciddor method) */
    muse_phys_air_to_vacuum(aPixtable, MUSE_PHYS_AIR_STANDARD);
  }

  /* create cube and cast to generic pointer to save code duplication */
  void *cube = NULL;
  muse_pixgrid *grid = NULL;
  if (aFormat == MUSE_CUBE_TYPE_EURO3D || aFormat == MUSE_CUBE_TYPE_EURO3D_X) {
    cpl_msg_info(__func__, "Resampling to final cube follows, output format "
                 "\"Euro3D\"");
    cpl_msg_indent_more();
    muse_euro3dcube *e3d = muse_resampling_euro3d(aPixtable, aParams);
    cpl_msg_indent_less();
    cpl_ensure_code(e3d, cpl_error_get_code());
    cube = e3d;
  } else if (aFormat == MUSE_CUBE_TYPE_FITS ||
      aFormat == MUSE_CUBE_TYPE_FITS_X || aFormat == MUSE_CUBE_TYPE_SDP) {
    cpl_msg_info(__func__, "Resampling to final cube follows, output format "
                 "\"FITS cube\"");
    cpl_msg_indent_more();
    muse_datacube *fits = muse_resampling_cube(aPixtable, aParams, &grid);
    cpl_msg_indent_less();
    cpl_ensure_code(fits, cpl_error_get_code());
    muse_postproc_qc_fwhm(aProcessing, fits);
    cube = fits;
  }

  cpl_array *filters = muse_cplarray_new_from_delimited_string(aFilter, ",");
  int i, ipos = 0, nfilters = cpl_array_get_size(filters);
  for (i = 0; i < nfilters; i++) {
    /* try to find and load the filter from a table */
    muse_table *filter = muse_table_load_filter(aProcessing,
                                                cpl_array_get_string(filters, i));
    if (!filter) {
      continue;
    }

    /* if we got a filter table, do the collapsing */
    muse_image *fov = NULL;
    if (aFormat == MUSE_CUBE_TYPE_EURO3D || aFormat == MUSE_CUBE_TYPE_EURO3D_X) {
      fov = muse_euro3dcube_collapse(cube, filter);
    } else if (aFormat == MUSE_CUBE_TYPE_FITS ||
        aFormat == MUSE_CUBE_TYPE_FITS_X || aFormat == MUSE_CUBE_TYPE_SDP) {
      if (getenv("MUSE_COLLAPSE_PIXTABLE") &&
          atoi(getenv("MUSE_COLLAPSE_PIXTABLE")) > 0) {
        /* create collapsed image directly from pixel table/grid */
        fov = muse_resampling_collapse_pixgrid(aPixtable, grid,
                                               (muse_datacube *)cube,
                                               filter, aParams);
      } else {
        fov = muse_datacube_collapse(cube, filter);
      }
    }
    muse_table_delete(filter);

    if (aFormat == MUSE_CUBE_TYPE_EURO3D_X || aFormat == MUSE_CUBE_TYPE_FITS_X) {
      if (!((muse_datacube *)cube)->recimages) {
        /* create empty structures before first entry */
        ((muse_datacube *)cube)->recimages = muse_imagelist_new();
        ((muse_datacube *)cube)->recnames = cpl_array_new(0, CPL_TYPE_STRING);
      }
      /* append image to cube/Euro3D structure */
      muse_imagelist_set(((muse_datacube *)cube)->recimages, fov,
                         muse_imagelist_get_size(((muse_datacube *)cube)->recimages));
      cpl_array_set_size(((muse_datacube *)cube)->recnames, ipos+1);
      cpl_array_set_string(((muse_datacube *)cube)->recnames, ipos,
                           cpl_array_get_string(filters, i));
    } else {
      /* we want NANs instead of DQ in the output images */
      muse_image_dq_to_nan(fov);
      /* duplicate cube header to be able to use its WCS info as the base */
      muse_utils_copy_modified_header(fov->header, fov->header, "OBJECT",
                                      cpl_array_get_string(filters, i));
      cpl_propertylist_update_string(fov->header, MUSE_HDR_FILTER,
                                      cpl_array_get_string(filters, i));
      if (aFormat == MUSE_CUBE_TYPE_SDP) {
        if (muse_idp_properties_update_fov(fov) != CPL_ERROR_NONE) {
            cpl_msg_warning(__func__, "Writing IDP keywords to field-of-view "
                            "image failed!");
        }
      }
      muse_processing_save_image(aProcessing, -1, fov, MUSE_TAG_IMAGE_FOV);
      muse_image_delete(fov);
    }
    ipos++;
  } /* for i (all filters) */
  cpl_array_delete(filters);
  muse_pixgrid_delete(grid);

#ifdef CREATE_IDP
  /* Collect IDP properties from the raw data frames and the data cube *
   * header                                                            */
  muse_idp_properties *properties =
      muse_idp_properties_collect(aProcessing, ((muse_datacube *)cube)->header,
                                  MUSE_TAG_CUBE_FINAL);

  /* Write formatted IDP properties to the data cube product header so *
   * that they are saved to the product file in the next step          */
  muse_idp_properties_update((muse_datacube *)cube)->header, properties);
#endif

  cpl_error_code rc = CPL_ERROR_NONE;
  if (aFormat == MUSE_CUBE_TYPE_EURO3D || aFormat == MUSE_CUBE_TYPE_EURO3D_X) {
    /* save the data and clean up */
    rc = muse_processing_save_cube(aProcessing, -1, cube, MUSE_TAG_CUBE_FINAL,
                                   MUSE_CUBE_TYPE_EURO3D);
    muse_euro3dcube_delete(cube);
  } else if (aFormat == MUSE_CUBE_TYPE_FITS || aFormat == MUSE_CUBE_TYPE_FITS_X ||
      aFormat == MUSE_CUBE_TYPE_SDP) {
    /* convert/remove the DQ cube */
    muse_datacube_convert_dq(cube);
    /* save the cube and clean up */
    if (aFormat == MUSE_CUBE_TYPE_SDP) {
      rc = muse_processing_save_cube(aProcessing, -1, cube, MUSE_TAG_CUBE_FINAL,
                                   MUSE_CUBE_TYPE_SDP);
    } else {
      rc = muse_processing_save_cube(aProcessing, -1, cube, MUSE_TAG_CUBE_FINAL,
                                   MUSE_CUBE_TYPE_FITS);
    }
    muse_datacube_delete(cube);
  }
#ifdef CREATE_IDP
  muse_idp_properties_delete(properties);
#endif
  return rc;
} /* muse_postproc_cube_resample_and_collapse() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Compute QC1 parameters for datacubes and save them in the FITS
           header.
  @param   aProcessing    the processing structure
  @param   aCube          the datacube
  @return  CPL_ERROR_NONE on success or a cpl_error_code on failure

  The prefix of the generated QC1 FITS keywords is set depending on the recipe
  name in the aProcessing structure.

  @note Part of this is copied in the image_fwhm tool.

  @algorithm This function detects objects in the central wavelength plane
             of the input datacube, derives the FWHM for each source in x-
             and y-direction, and converts the FWHM from pixel scale to
             arcsec units which are saved in the header of the input cube.

  @error{return CPL_ERROR_NULL_INPUT, aProcessing or aCube are NULL}
  @error{return CPL_ERROR_DATA_NOT_FOUND, no sources were detected}
  @error{output info message\, return CPL_ERROR_NONE,
         unknown recipe called this function}
 */
/*---------------------------------------------------------------------------*/
cpl_error_code
muse_postproc_qc_fwhm(muse_processing *aProcessing, muse_datacube *aCube)
{
  cpl_ensure_code(aProcessing && aCube, CPL_ERROR_NULL_INPUT);

  /* determine which FITS keyword prefix to take */
  const char *prefix = "";
  if (!strncmp(aProcessing->name, "muse_scipost", 13)) {
    prefix = QC_POST_PREFIX_SCIPOST;
  } else if (!strncmp(aProcessing->name, "muse_exp_combine", 17)) {
    prefix = QC_POST_PREFIX_EXPCOMBINE;
  } else if (!strncmp(aProcessing->name, "muse_standard", 14)) {
    prefix = QC_POST_PREFIX_STANDARD;
  } else if (!strncmp(aProcessing->name, "muse_astrometry", 16)) {
    prefix = QC_POST_PREFIX_ASTROMETRY;
  } else {
    cpl_msg_info(__func__, "Recipe \"%s\" found, not generating QC1 keywords",
                 aProcessing->name);
    return CPL_ERROR_NONE;
  }

  /* get image from central plane of cube and use it to do object detection *
   * and FWHM estimation                                                    */
  int plane = cpl_imagelist_get_size(aCube->data) / 2;
  cpl_image *cim = cpl_imagelist_get(aCube->data, plane);
  double dsigmas[] = {/*50., 30., 10., 8., 6.,*/ 5., 4., 3. };
  int ndsigmas = sizeof(dsigmas) / sizeof(double);
  cpl_vector *vsigmas = cpl_vector_wrap(ndsigmas, dsigmas);
  cpl_size isigma = -1;
  cpl_errorstate prestate = cpl_errorstate_get();
  cpl_apertures *apertures = cpl_apertures_extract(cim, vsigmas, &isigma);
  cpl_vector_unwrap(vsigmas);

  /* add the first QC parameters, no matter what the detection did */
  char keyword[KEYWORD_LENGTH];
  cpl_boolean loglambda = !strncmp(muse_pfits_get_ctype(aCube->header, 3),
                                   "AWAV-LOG", 9);
  double crpix3 = muse_pfits_get_crpix(aCube->header, 3),
         cd33 = muse_pfits_get_cd(aCube->header, 3, 3),
         crval3 = muse_pfits_get_crval(aCube->header, 3),
         lambda = loglambda
                ? crval3 * exp((plane + 1. - crpix3) * cd33 / crval3)
                : (plane + 1. - crpix3) * cd33 + crval3;
  snprintf(keyword, KEYWORD_LENGTH, QC_POST_LAMBDA, prefix);
  cpl_propertylist_update_float(aCube->header, keyword, lambda);

  /* now see, if we detected anything */
  if (!apertures || !cpl_errorstate_is_equal(prestate)) {
    /* set up number and a fake entry (source number zero and negative FWHMs) */
    snprintf(keyword, KEYWORD_LENGTH, QC_POST_NDET, prefix);
    cpl_propertylist_update_int(aCube->header, keyword, 0); /* zero! */

    snprintf(keyword, KEYWORD_LENGTH, QC_POST_POSX, prefix, 0);
    cpl_propertylist_update_float(aCube->header, keyword, -1.);
    snprintf(keyword, KEYWORD_LENGTH, QC_POST_POSY, prefix, 0);
    cpl_propertylist_update_float(aCube->header, keyword, -1.);
    snprintf(keyword, KEYWORD_LENGTH, QC_POST_FWHMX, prefix, 0);
    cpl_propertylist_update_float(aCube->header, keyword, -1.);
    snprintf(keyword, KEYWORD_LENGTH, QC_POST_FWHMY, prefix, 0);
    cpl_propertylist_update_float(aCube->header, keyword, -1.);
    /* swallow the cpl_aperture error, which is too cryptic for users */
    cpl_errorstate_set(prestate);
    cpl_msg_warning(__func__, "No sources found for FWHM measurement down to "
                    "%.1f sigma limit on plane %d, QC parameters will not "
                    "contain useful information", dsigmas[ndsigmas-1], plane+1);
    return CPL_ERROR_DATA_NOT_FOUND;
  } /* if no detections */
  int ndet = cpl_apertures_get_size(apertures);
  snprintf(keyword, KEYWORD_LENGTH, QC_POST_NDET, prefix);
  cpl_propertylist_update_int(aCube->header, keyword, ndet); /* zero! */

  /* get some kind of WCS for conversion of FWHM from pixels to arcsec, *
   * by default assume WFM and x=RA and y=DEC                           */
  double cd11 = kMuseSpaxelSizeX_WFM, cd12 = 0., cd21 = 0.,
         cd22 = kMuseSpaxelSizeY_WFM;
  cpl_errorstate es = cpl_errorstate_get();
  cpl_wcs *wcs = cpl_wcs_new_from_propertylist(aCube->header);
  if (!cpl_errorstate_is_equal(es)) {
    cpl_errorstate_set(es); /* swallow possible errors from creating the WCS */
  }
  if (!wcs || !strncasecmp(muse_pfits_get_ctype(aCube->header, 1), "PIXEL", 5)) {
    if (muse_pfits_get_mode(aCube->header) == MUSE_MODE_NFM_AO_N) { /* NFM scaling */
      cd11 = kMuseSpaxelSizeX_NFM;
      cd22 = kMuseSpaxelSizeY_NFM;
    }
  } else {
    const cpl_matrix *cd = cpl_wcs_get_cd(wcs);
    /* take the absolute and scale by 3600 to get positive arcseconds */
    cd11 = fabs(cpl_matrix_get(cd, 0, 0)) * 3600.,
    cd12 = fabs(cpl_matrix_get(cd, 0, 1)) * 3600.,
    cd21 = fabs(cpl_matrix_get(cd, 1, 0)) * 3600.,
    cd22 = fabs(cpl_matrix_get(cd, 1, 1)) * 3600.;
  }
  cpl_wcs_delete(wcs); /* rely on internal NULL check */

  /* Compute FWHM of all apertures; collect them *
   * in an image for statistical computations    */
  cpl_image *statimage = cpl_image_new(ndet, 2, CPL_TYPE_DOUBLE);
#if 0
  printf("index RA_FWHM DEC_FWHM\n");
#endif
  int n, nbad = 0;
  for (n = 1; n <= ndet; n++) {
    cpl_size xcen = lround(cpl_apertures_get_centroid_x(apertures, n)),
             ycen = lround(cpl_apertures_get_centroid_y(apertures, n));
    double x, y;
    cpl_errorstate state = cpl_errorstate_get();
    cpl_image_get_fwhm(cim, xcen, ycen, &x, &y);
    const char *fwhmcomment = NULL;
    if (x < 0 || y < 0 || !cpl_errorstate_is_equal(state)) {
      /* something's weird with this source */
      x = -1.;
      y = -1.;
      /* do not count these in the statistics */
      cpl_image_reject(statimage, n, 1);
      cpl_image_reject(statimage, n, 2);
      fwhmcomment = "[arcsec] failure determining FWHM";
      nbad++;
      /* suppress the error state */
      cpl_errorstate_set(state);
    } else {
      /* linear WCS scaling */
      x = cd11 * x + cd12 * y;
      y = cd22 * y + cd21 * x;
      cpl_image_set(statimage, n, 1, x);
      cpl_image_set(statimage, n, 2, y);
    }
#if 0
    printf("%4d  %f %f\n", n, x, y);
#endif

    /* generate and write FITS keywords */
    snprintf(keyword, KEYWORD_LENGTH, QC_POST_POSX, prefix, n);
    cpl_propertylist_update_float(aCube->header, keyword, xcen);
    snprintf(keyword, KEYWORD_LENGTH, QC_POST_POSY, prefix, n);
    cpl_propertylist_update_float(aCube->header, keyword, ycen);
    snprintf(keyword, KEYWORD_LENGTH, QC_POST_FWHMX, prefix, n);
    cpl_propertylist_update_float(aCube->header, keyword, x);
    if (fwhmcomment) {
      cpl_propertylist_set_comment(aCube->header, keyword, fwhmcomment);
    }
    snprintf(keyword, KEYWORD_LENGTH, QC_POST_FWHMY, prefix, n);
    cpl_propertylist_update_float(aCube->header, keyword, y);
    if (fwhmcomment) {
      cpl_propertylist_set_comment(aCube->header, keyword, fwhmcomment);
    }
  } /* for n (aperture number) */
#if 0
  fflush(stdout);
#endif
  cpl_apertures_delete(apertures);

  /* add simple FWHM statistics as well */
  snprintf(keyword, KEYWORD_LENGTH, QC_POST_FWHM_NVALID, prefix);
  cpl_propertylist_update_int(aCube->header, keyword, ndet - nbad);
  cpl_errorstate state = cpl_errorstate_get();
  cpl_stats *s = cpl_stats_new_from_image(statimage,
                                          CPL_STATS_MEDIAN | CPL_STATS_MAD);
  cpl_boolean error = !cpl_errorstate_is_equal(state);
  cpl_errorstate_set(state); /* swallow the error again */
  snprintf(keyword, KEYWORD_LENGTH, QC_POST_FWHM_MEDIAN, prefix);
  if (error || ndet < 3) {
    cpl_propertylist_update_float(aCube->header, keyword, 0.);
  } else {
    cpl_propertylist_update_float(aCube->header, keyword, cpl_stats_get_median(s));
  }
  snprintf(keyword, KEYWORD_LENGTH, QC_POST_FWHM_MAD, prefix);
  if (error || ndet < 3) {
    cpl_propertylist_update_float(aCube->header, keyword, 0.);
  } else {
    cpl_propertylist_update_float(aCube->header, keyword, cpl_stats_get_mad(s));
  }
  cpl_stats_delete(s);
  cpl_image_delete(statimage);
  cpl_msg_info(__func__, "Computed FWHM QC parameters for %d of %d source%s "
               "found down to the %.1f sigma threshold on plane %d",
               ndet - nbad, ndet, ndet == 1 ? "" : "s", dsigmas[isigma],
               plane + 1);

  return CPL_ERROR_NONE;
} /* muse_postproc_qc_fwhm() */

/**@}*/
