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

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

/*----------------------------------------------------------------------------*
 *                             Includes                                       *
 *----------------------------------------------------------------------------*/
#include <string.h>
#include <strings.h>
#include "muse_postproc.h"
#include "muse_instrument.h"

#include "muse_astro.h"
#include "muse_dar.h"
#include "muse_flux.h"
#include "muse_imagelist.h"
#include "muse_pfits.h"
#include "muse_rvcorrect.h"
#include "muse_utils.h"
#include "muse_wcs.h"

/*----------------------------------------------------------------------------*/
/**
 * @defgroup muse_postproc       Post-processing functions
 *
 * This group 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.

  @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);
  cpl_table_delete(aProp->response);
  cpl_table_delete(aProp->extinction);
  cpl_table_delete(aProp->telluric);
  cpl_propertylist_delete(aProp->wcs);
  muse_sky_master_delete(aProp->sky);
  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;
  }
  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, "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}
 */
/*----------------------------------------------------------------------------*/
static 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    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    aSkyOut   sky output structure
  @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 aSkyOut structure 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}
 */
/*----------------------------------------------------------------------------*/
void *
muse_postproc_process_exposure(muse_postproc_properties *aProp,
                               unsigned int aIndex,
                               muse_postproc_sky_outputs *aSkyOut)
{
  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);

  /* XXX try to recognize a lab exposure by missing RA/DEC or weird   *
   *     airmass; in the future this should be a template-based check */
  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);
  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() : "");
    /* 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;
      cpl_msg_info(__func__, "Carrying out DAR %s", dorefine ? "correction" : "check");
      double maxshift = 0;
      rc = muse_dar_check(pt, &maxshift, dorefine, NULL);
      if (rc != CPL_ERROR_NONE) {
        cpl_msg_warning(__func__, "Maximum detected shift %f\'\' (rc = %d: %s)",
                        maxshift, rc, cpl_error_get_message());
      } else {
        cpl_msg_info(__func__, "Maximum detected shift %f\'\'", maxshift);
      }
    } /* if DAR checking */
  } /* if not labdata but WFM */

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

  cpl_table *sky_cont = NULL,
            *sky_lines = NULL;
  if (muse_pixtable_is_fluxcal(pt) &&
      aProp->skymethod == MUSE_POSTPROC_SKYMETHOD_MODEL &&
      aProp->sky && aProp->sky->lines) {
    /* 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);
    }
    /* Create white light image and sky mask */
    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 = 15.;
    muse_datacube *cube = muse_resampling_cube(sky_pt, params, NULL);
    if (!cube) {
      cpl_msg_error(__func__, "Could not create cube for whitelight image");
      muse_pixtable_delete(sky_pt);
      muse_pixtable_delete(pt);
      return NULL;
    }
    cpl_table *fwhite = muse_table_load_filter(NULL, "white");
    muse_image *whitelight = muse_datacube_collapse(cube, fwhite);
    cpl_msg_indent_less();
    muse_mask *sky_mask = muse_sky_create_skymask(whitelight,
                                                  aProp->skymodel_params.fraction,
            /* XXX hardcode prefix for now... */  "ESO QC SCIPOST");

    /* Apply mask and create spectrum */
    cpl_table_select_all(sky_pt->table);
    muse_pixtable_and_selected_mask(sky_pt, sky_mask);
    cpl_table *spectrum = muse_resampling_spectrum(sky_pt, aProp->skymodel_params.sampling);

    /* Fit sky lines spectrum */
    cpl_array *lambda = muse_cpltable_extract_column(spectrum, "lambda");
    double lambda_low = cpl_array_get_min(lambda);
    double lambda_high = cpl_array_get_max(lambda);
    /* duplicate input sky lines, to crop and save them, for this one exposure */
    sky_lines = cpl_table_duplicate(aProp->sky->lines);
    muse_sky_lines_set_range(sky_lines, lambda_low-5, lambda_high+5);

    cpl_array *data = muse_cpltable_extract_column(spectrum, "data");
    cpl_array *stat = muse_cpltable_extract_column(spectrum, "stat");
    /* do the fit, ignoring possible errors */
    prestate = cpl_errorstate_get();
    muse_sky_master *master = muse_sky_master_fit(lambda, data, stat, sky_lines);
    if (!cpl_errorstate_is_equal(prestate)) {
      cpl_errorstate_dump(prestate, CPL_FALSE, NULL);
      cpl_errorstate_set(prestate);
    }
    cpl_table_delete(sky_lines);
    sky_lines = cpl_table_duplicate(master->lines); /* keep the modified lines */

    /* Create continuum */
    if (!aProp->sky->continuum) {
      cpl_msg_info(__func__, "No sky continuum given, create it from the data");
      /* remove the continuum from the master sky object,    *
       * otherwise it messes up the continuum creation here! */
      cpl_table_delete(master->continuum);
      master->continuum = NULL;
      muse_sky_subtract_pixtable(sky_pt, master, aProp->sky->lsf);
      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");
    } else {
      cpl_msg_info(__func__, "Using sky continuum given as input");
      sky_cont = cpl_table_duplicate(aProp->sky->continuum);
    }

    /* clean up */
    cpl_array_unwrap(lambda);
    cpl_array_unwrap(data);
    cpl_array_unwrap(stat);
    muse_sky_master_delete(master);
    if (aSkyOut) {
      aSkyOut->mask = sky_mask;
      aSkyOut->spectrum = spectrum;
    } else {
      muse_mask_delete(sky_mask);
      cpl_table_delete(spectrum);
    }
    muse_image_delete(whitelight);
    cpl_table_delete(fwhite);
    muse_resampling_params_delete(params);
    muse_datacube_delete(cube);
    muse_pixtable_delete(sky_pt);
  } /* if MUSE_POSTPROC_SKYMETHOD_MODEL */
  /* 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 (muse_pixtable_is_fluxcal(pt) && aProp->sky &&
      ((aProp->skymethod == MUSE_POSTPROC_SKYMETHOD_NONE) ||
       (aProp->skymethod == MUSE_POSTPROC_SKYMETHOD_MODEL))) {
    /* create a new (partially duplicate) master sky that we are going to use */
    muse_sky_master *sky = muse_sky_master_new();
    /* leave sky->lsf NULL, since that is not used for subtraction */
    if (aProp->sky->continuum) {
      cpl_msg_debug(__func__, "doing sky continuum subtraction with input "
                    "spectrum");
      sky->continuum = cpl_table_duplicate(aProp->sky->continuum);
    } else if (sky_cont) {
      cpl_msg_debug(__func__, "doing sky continuum subtraction with created "
                    "spectrum");
      sky->continuum = cpl_table_duplicate(sky_cont);
    }
    if (sky_lines) {
      cpl_msg_debug(__func__, "doing sky line subtraction with created list");
      sky->lines = cpl_table_duplicate(sky_lines);
    } else if (aProp->sky->lines) {
      cpl_msg_debug(__func__, "doing sky line subtraction with input list");
      sky->lines = cpl_table_duplicate(aProp->sky->lines);
    }
    cpl_msg_debug(__func__, "doing sky subtraction by spectrum modeling");
    rc = muse_sky_subtract_pixtable(pt, sky, aProp->sky->lsf);
    muse_sky_master_delete(sky);
  } /* 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);
  }
  if (aProp->skymethod == MUSE_POSTPROC_SKYMETHOD_ROWBYROW) {
    /* apply the row-by-row sky subtraction; needed for row-by-row */
    cpl_msg_debug(__func__, "doing sky subtraction using row-by-row fitting");
    /* convert pixel table into images, one per IFU */
    muse_imagelist *list = muse_pixtable_to_imagelist(pt);
    unsigned int k, n = muse_imagelist_get_size(list);
    #pragma omp parallel for default(none)                                     \
            shared(aProp, list, n)
    for (k = 0; k < n; k++) {
      muse_image *image = muse_imagelist_get(list, k);
      muse_sky_subtract_rowbyrow_mask(image, NULL);
      muse_sky_subtract_rowbyrow(image, NULL, 3., 4);
    } /* for all IFUs / images */
    muse_pixtable_from_imagelist(pt, list);
    muse_imagelist_delete(list);
  } /* if row-by-row sky subtraction */

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

    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, aProp->detsigma, aProp->centroid, wcs);
    rc = muse_wcs_solve(wcs, reftable, aProp->radius, aProp->faccuracy,
                        aProp->niter, aProp->rejsigma);
    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);
    }
    return wcs;
  } /* if MUSE_POSTPROC_ASTROMETRY */

  rc = muse_wcs_project_tan(pt, aProp->wcs);
  cpl_msg_debug(__func__, "Astrometry correction returned rc=%d: %s", rc,
                rc != CPL_ERROR_NONE ? cpl_error_get_message() : "");

  return pt;
} /* muse_postproc_process_exposure() */

/*---------------------------------------------------------------------------*/
/**
  @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 && 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));
    }
    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, CPL_ERROR_ILLEGAL_INPUT);

  /* 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) {
    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 */
    cpl_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) {
      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);
      }
    }
    cpl_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));
      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);

  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) {
    /* convert/remove the DQ cube */
    muse_datacube_convert_dq(cube);
    /* save the cube and clean up */
    rc = muse_processing_save_cube(aProcessing, -1, cube, MUSE_TAG_CUBE_FINAL,
                                   MUSE_CUBE_TYPE_FITS);
    muse_datacube_delete(cube);
  }
  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.

  XXX 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 */
  char prefix[KEYWORD_LENGTH] = "";
  if (!strncmp(aProcessing->name, "muse_scipost", 13)) {
    strncpy(prefix, QC_POST_PREFIX_SCIPOST, KEYWORD_LENGTH);
  } else if (!strncmp(aProcessing->name, "muse_exp_combine", 17)) {
    strncpy(prefix, QC_POST_PREFIX_EXPCOMBINE, KEYWORD_LENGTH);
  } else if (!strncmp(aProcessing->name, "muse_standard", 14)) {
    strncpy(prefix, QC_POST_PREFIX_STANDARD, KEYWORD_LENGTH);
  } else if (!strncmp(aProcessing->name, "muse_astrometry", 16)) {
    strncpy(prefix, QC_POST_PREFIX_ASTROMETRY, KEYWORD_LENGTH);
  } 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);
  if (!apertures || !cpl_errorstate_is_equal(prestate)) {
    /* just set up two fake entries, with number zero and negative FWHMs: */
    char keyword[KEYWORD_LENGTH];
    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;
  }
  int ndet = cpl_apertures_get_size(apertures);
  cpl_msg_info(__func__, "Computing FWHM QC parameters for %d source%s found "
               "down to the %.1f sigma threshold on plane %d", ndet,
               ndet == 1 ? "" : "s", dsigmas[isigma], plane + 1);

  /* 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(cpl_propertylist_get_string(aCube->header, "CTYPE1"),
                   "PIXEL", 5)) {
    cpl_msg_debug(__func__, "QC1 FWHM parameter estimation (%d sources): "
                  "simple conversion to arcsec (CTYPE=%s/%s)!", ndet,
                  cpl_propertylist_get_string(aCube->header, "CTYPE1"),
                  cpl_propertylist_get_string(aCube->header, "CTYPE2"));
    if (muse_pfits_get_mode(aCube->header) > MUSE_MODE_WFM_AO_N) { /* NFM scaling */
      cd11 = kMuseSpaxelSizeX_WFM;
      cd22 = kMuseSpaxelSizeY_WFM;
    }
  } 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_msg_debug(__func__, "QC1 FWHM parameter estimation (%d sources): "
                  "full conversion to arcsec (CD=%f,%f,%f,%f)", ndet,
                  cd11, cd12, cd21, cd22);
  }
  cpl_wcs_delete(wcs); /* rely on internal NULL check */

  /* Compute FWHM of all apertures */
#if 0
  printf("index RA_FWHM DEC_FWHM\n");
#endif
  int n;
  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_image_get_fwhm(cim, xcen, ycen, &x, &y);

    /* linear WCS scaling */
    x = cd11 * x + cd12 * y;
    y = cd22 * y + cd21 * x;
#if 0
    printf("%4d  %f %f\n", n, x, y);
#endif

    /* generate and write FITS keywords */
    char keyword[KEYWORD_LENGTH];
    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);
    snprintf(keyword, KEYWORD_LENGTH, QC_POST_FWHMY, prefix, n);
    cpl_propertylist_update_float(aCube->header, keyword, y);
  } /* for n (aperture number) */
#if 0
  fflush(stdout);
#endif
  cpl_apertures_delete(apertures);

  return CPL_ERROR_NONE;
} /* muse_postproc_qc_fwhm() */

/**@}*/
