/* -*- 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-2015 European Southern Observatory
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

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

/*----------------------------------------------------------------------------*
 *                             Includes                                       *
 *----------------------------------------------------------------------------*/
#include <cpl.h>
#include <string.h>

#include "muse_xcombine.h"

#include "muse_pfits.h"
#include "muse_wcs.h"
#include "muse_utils.h"
#include "muse_data_format_z.h"

/*----------------------------------------------------------------------------*/
/**
 * @defgroup muse_xcombine     Xposure combination
 */
/*----------------------------------------------------------------------------*/

/**@{*/

/*----------------------------------------------------------------------------*/
/**
  @private
  @brief   Create table with exposure information needed to compute exposure
           weighting.
  @param   aPixtables   the NULL-terminated array of pixel tables to weight
  @return  the CPL table with the info on success

  The columns "EXPTIME", "AG_AVG", "IA_FWHM", and "DIMM" can be used to compute
  relative weighting of exposures. The column "AG_RMS" can be used as error
  estimate on the "AG_AVG" column.

  @note    This function only does minimal error checking.
 */
/*----------------------------------------------------------------------------*/
static cpl_table *
muse_xcombine_weights_infotable(muse_pixtable **aPixtables)
{
  unsigned int npt = 0;
  while (aPixtables[npt++]) ; /* count tables, including first NULL table */
  --npt; /* subtract NULL table */

  /* create table for the values */
  cpl_table *table = cpl_table_new(npt);
  cpl_table_new_column(table, "EXPTIME", CPL_TYPE_DOUBLE);
  cpl_table_new_column(table, "AGX_AVG", CPL_TYPE_DOUBLE);
  cpl_table_new_column(table, "AGX_RMS", CPL_TYPE_DOUBLE);
  cpl_table_new_column(table, "AGY_AVG", CPL_TYPE_DOUBLE);
  cpl_table_new_column(table, "AGY_RMS", CPL_TYPE_DOUBLE);
  cpl_table_new_column(table, "IA_FWHM", CPL_TYPE_DOUBLE);
  cpl_table_new_column(table, "DIMM_START", CPL_TYPE_DOUBLE);
  cpl_table_new_column(table, "DIMM_END", CPL_TYPE_DOUBLE);

  /* fill the table with the values */
  cpl_errorstate prestate = cpl_errorstate_get();
  unsigned int i;
  for (i = 0; i < npt; i++) {
    /* exposure time */
    cpl_errorstate state = cpl_errorstate_get();
    double value = muse_pfits_get_exptime(aPixtables[i]->header);
    if (cpl_errorstate_is_equal(state)) {
      cpl_table_set_double(table, "EXPTIME", i, value);
    }

    /* AG values */
    state = cpl_errorstate_get();
    value = muse_pfits_get_agx_avg(aPixtables[i]->header);
    if (cpl_errorstate_is_equal(state)) {
      cpl_table_set_double(table, "AGX_AVG", i, value);
    }
    state = cpl_errorstate_get();
    value = muse_pfits_get_agx_rms(aPixtables[i]->header);
    if (cpl_errorstate_is_equal(state)) {
      cpl_table_set_double(table, "AGX_RMS", i, value);
    }
    state = cpl_errorstate_get();
    value = muse_pfits_get_agy_avg(aPixtables[i]->header);
    if (cpl_errorstate_is_equal(state)) {
      cpl_table_set_double(table, "AGY_AVG", i, value);
    }
    state = cpl_errorstate_get();
    value = muse_pfits_get_agy_rms(aPixtables[i]->header);
    if (cpl_errorstate_is_equal(state)) {
      cpl_table_set_double(table, "AGY_RMS", i, value);
    }

    /* IA value */
    state = cpl_errorstate_get();
    value = muse_pfits_get_ia_fwhm(aPixtables[i]->header);
    if (cpl_errorstate_is_equal(state)) {
      cpl_table_set_double(table, "IA_FWHM", i, value);
    }

    /* DIMM values */
    state = cpl_errorstate_get();
    value = muse_pfits_get_fwhm_start(aPixtables[i]->header);
    if (cpl_errorstate_is_equal(state) && value > 0.) {
      cpl_table_set_double(table, "DIMM_START", i, value);
    }
    state = cpl_errorstate_get();
    value = muse_pfits_get_fwhm_end(aPixtables[i]->header);
    if (cpl_errorstate_is_equal(state) && value > 0.) {
      cpl_table_set_double(table, "DIMM_END", i, value);
    }
  } /* for i (table index) */
  cpl_errorstate_set(prestate); /* swallow all errors */

  /* compute AG_AVG = (AGX + AGY) / 2 */
  cpl_table_duplicate_column(table, "AG_AVG", table, "AGX_AVG");
  cpl_table_add_columns(table, "AG_AVG", "AGY_AVG");
  cpl_table_multiply_scalar(table, "AG_AVG", 0.5);
  /* compute AG_RMS = sqrt(AGX_RMS**2 + AGY_RMS**2) */
  cpl_table_duplicate_column(table, "AG_RMS", table, "AGX_RMS");
  cpl_table_power_column(table, "AG_RMS", 2); /* **2 */
  cpl_table_duplicate_column(table, "AG2_RMS", table, "AGY_RMS");
  cpl_table_power_column(table, "AG2_RMS", 2); /* **2 */
  cpl_table_add_columns(table, "AG_RMS", "AG2_RMS");
  cpl_table_erase_column(table, "AG2_RMS");
  cpl_table_power_column(table, "AG_RMS", 0.5); /* sqrt() */
  /* compute DIMM = (DIMM_START + DIMM_END) / 2 */
  cpl_table_duplicate_column(table, "DIMM", table, "DIMM_START");
  cpl_table_add_columns(table, "DIMM", "DIMM_END");
  cpl_table_multiply_scalar(table, "DIMM", 0.5);

#if 0
  printf("%s:\n", __func__);
  const char *p, *cols[] = { "EXPTIME", "AG_AVG", "IA_FWHM", "DIMM", NULL };
  for (i = 0, p = cols[i] ; p; p = cols[i++ + 1]) {
    prestate = cpl_errorstate_get();
    printf("%s: %.3f +/- %.3f (%"CPL_SIZE_FORMAT" invalid)\n", p,
           cpl_table_get_column_mean(table, p), cpl_table_get_column_stdev(table, p),
           cpl_table_count_invalid(table, p));
    /* swallow errors due to completely invalid column */
    cpl_errorstate_set(prestate);
  }
  cpl_table_dump(table, 0, npt, stdout);
  fflush(stdout);
#endif

  return table;
} /* muse_xcombine_weights_infotable() */

/*----------------------------------------------------------------------------*/
/**
  @brief   compute the weights for combination of two or more exposures
  @param   aPixtables   the NULL-terminated array of pixel tables to weight
  @param   aWeighting   the weighting scheme to use
  @return  CPL_ERROR_NONE on success any other value on failure
  @remark  The weights are applied to the input pixel tables, adding another
           column. If a table column MUSE_PIXTABLE_WEIGHT already exists, the
           values it contains are overwritten.
  @remark This function adds a FITS header (@ref MUSE_HDR_PT_WEIGHTED) with the
          boolean value 'T' to the pixel table, for information.

  For exposure time weighting (aWeighting == MUSE_XCOMBINE_EXPTIME), compute
  the ratio of the exposure time of the current exposure with the one from the
  first exposure, and store this ratio as weight in a new table column. For
  FWHM-based weighting (aWeighting == MUSE_XCOMBINE_FWHM), use the DIMM seeing
  information, and multiply the FWHM ratio by the exposure-weight to form the
  final weight.

  If aWeighting is MUSE_XCOMBINE_NONE, this function only checks for valid
  inputs, but then returns without doing anything.

  @error{return CPL_ERROR_NULL_INPUT, aPixtables is NULL}
  @error{return CPL_ERROR_ILLEGAL_INPUT,
         there are less than 2 input pixel tables}
  @error{output warning and return CPL_ERROR_UNSUPPORTED_MODE,
         an unknown weighting scheme was given}
  @error{return CPL_ERROR_INCOMPATIBLE_INPUT,
         EXPTIME is not set in the header of the first pixel table}
  @error{weights for these tables are set to zero,
         the exposure time header is missing from one or more input pixel tables}
  @error{output warning and set CPL_ERROR_DATA_NOT_FOUND\, switch to EXPTIME-weighting\, but return CPL_ERROR_NONE,
         all FWHM header entries are missing from one or more input pixel tables although MUSE_XCOMBINE_FWHM is requrested}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_xcombine_weights(muse_pixtable **aPixtables, muse_xcombine_types aWeighting)
{
  cpl_ensure_code(aPixtables, CPL_ERROR_NULL_INPUT);
  unsigned int npt = 0;
  while (aPixtables[npt++]) ; /* count tables, including first NULL table */
  cpl_ensure_code(--npt > 1, CPL_ERROR_ILLEGAL_INPUT); /* subtract NULL table */
  if (aWeighting == MUSE_XCOMBINE_NONE) {
    cpl_msg_info(__func__, "%d tables, not weighting them", npt);
    return CPL_ERROR_NONE;
  }
  if (aWeighting != MUSE_XCOMBINE_EXPTIME && aWeighting != MUSE_XCOMBINE_FWHM) {
    cpl_msg_warning(__func__, "Unknown exposure weighting scheme (%d)",
                    aWeighting);
    return cpl_error_set(__func__, CPL_ERROR_UNSUPPORTED_MODE);
  }

  /* create table with weighting information: exposure times, *
   * and several seeing FWHM estimators from the FITS header  */
  cpl_table *tinfo = muse_xcombine_weights_infotable(aPixtables);
  int err;
  double exptime0 = cpl_table_get_double(tinfo, "EXPTIME", 0, &err);
  if (err || exptime0 == 0.0) {
    cpl_table_delete(tinfo);
    return cpl_error_set(__func__, CPL_ERROR_INCOMPATIBLE_INPUT);
  }
  /* select FWHM estimator for FWHM-based weighting */
  double fwhm0 = 0.;
  const char *fwhmcolumn = NULL,
             *fwhmerrcol = NULL;
  if (aWeighting == MUSE_XCOMBINE_FWHM) {
    cpl_size ninvalid = cpl_table_count_invalid(tinfo, "AG_AVG");
    if (ninvalid > 0) { /* if any entry is invalid */
      ninvalid = cpl_table_count_invalid(tinfo, "IA_FWHM");
      if (ninvalid > 0) { /* any more entry invalid */
        ninvalid = cpl_table_count_invalid(tinfo, "DIMM");
        if (!ninvalid) { /* success at last */
          fwhmcolumn = "DIMM";
          cpl_msg_info(__func__, "%d tables to be weighted using EXPTIME & FWHM "
                       "(using DIMM measurements)", npt);
        }
      } else {
        fwhmcolumn = "IA_FWHM";
        cpl_msg_info(__func__, "%d tables to be weighted using EXPTIME & FWHM "
                     "(using active optics image analysis)", npt);
      }
    } else {
      fwhmcolumn = "AG_AVG";
      fwhmerrcol = "AG_RMS";
      cpl_msg_info(__func__, "%d tables to be weighted using EXPTIME & FWHM "
                   "(using auto-guider info)", npt);
    }
    if (!fwhmcolumn) {
      cpl_msg_warning(__func__, "%d tables to be weighted using EXPTIME.", npt);
      cpl_msg_warning(__func__, "(FWHM-based weighting was requested but cannot"
                      " be carried due to incomplete FITS headers in some "
                      "exposures.)");
      cpl_error_set_message(__func__, CPL_ERROR_DATA_NOT_FOUND, "missing FITS "
                            "headers for FWHM-based exposure weighting");
    } else {
      fwhm0 = cpl_table_get_double(tinfo, fwhmcolumn, 0, &err);
    }
  } else {
    /* simple exptime-time weighting */
    cpl_msg_info(__func__, "%d tables to be weighted using EXPTIME.", npt);
  }

  /* add and fill the "weight" column in all pixel tables */
  unsigned int i;
  for (i = 0; i < npt; i++) {
    double exptime = cpl_table_get_double(tinfo, "EXPTIME", i, &err),
           weight = exptime / exptime0;
    if (!cpl_table_has_column(aPixtables[i]->table, MUSE_PIXTABLE_WEIGHT)) {
      cpl_table_new_column(aPixtables[i]->table, MUSE_PIXTABLE_WEIGHT,
                           CPL_TYPE_FLOAT);
    }
    /* modify the "weight" column depending on ambient seeing */
    char *fwhmstr = NULL;
    if (fwhmcolumn) {
      double fwhm = cpl_table_get_double(tinfo, fwhmcolumn, i, &err),
             fwhmerr = fwhmerrcol
                     ? cpl_table_get_double(tinfo, fwhmerrcol, i, NULL) : 0.;
      weight *= fwhm0 / fwhm;
      if (fwhmerrcol) {
        fwhmstr = cpl_sprintf(", FWHM = %.2f +/- %.2f", fwhm, fwhmerr);
      } else {
        fwhmstr = cpl_sprintf(", FWHM = %.2f", fwhm);
      }
    }
    cpl_msg_debug(__func__, "Table %d, weight = %f (EXPTIME = %f%s)", i+1, weight,
                  exptime, fwhmstr ? fwhmstr : "");
    cpl_free(fwhmstr);
    cpl_table_fill_column_window_float(aPixtables[i]->table,
                                       MUSE_PIXTABLE_WEIGHT,
                                       0, muse_pixtable_get_nrow(aPixtables[i]),
                                       weight);
    /* add the status header */
    cpl_propertylist_update_bool(aPixtables[i]->header, MUSE_HDR_PT_WEIGHTED,
                                 CPL_TRUE);
    cpl_propertylist_set_comment(aPixtables[i]->header, MUSE_HDR_PT_WEIGHTED,
                                 MUSE_HDR_PT_WEIGHTED_COMMENT);
  } /* for i (table index) */
  cpl_table_delete(tinfo);

  return CPL_ERROR_NONE;
} /* muse_xcombine_weights() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Get offsets and scale from table row with matching DATE-OBS entry.
  @param   aOffsets   table with coordinate offsets to apply
  @param   aDateObs   the DATE-OBS string to compare
  @return  a three-element double array, with the two coordinate offsets or NULL
           on error
  @note    The returned pointer has to be deallocated with cpl_free() after use.
  @note    By default (if the optional MUSE_OFFSETS_FSCALE column does not
           exist) or on error, the third element of the returned array is NAN, so
           that no flux scaling occurs (when applying the output of this function
           to a pixel table).

  @error{set CPL_ERROR_NULL_INPUT\, return NULL, aOffsets and/or aDateObs are NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         aDateObs has a length of less than 19 or more than 68 characters}
  @error{set NAN for each invalid value\, output a warning,
         the value(s) in the columns MUSE_OFFSETS_DRA and/or MUSE_OFFSETS_DDEC could not be read}
  @error{no error is raised\, just return NULL,
         an entry with aDateObs is not found in aOffsets}
 */
/*----------------------------------------------------------------------------*/
double *
muse_xcombine_find_offsets(const cpl_table *aOffsets, const char *aDateObs)
{
  const char *id = "muse_xcombine_tables"; /* pretend to be in that function */
  cpl_ensure(aOffsets && aDateObs, CPL_ERROR_NULL_INPUT, NULL);
  /* DATE-OBS format allowed by FITS are YYYY-MM-DDThh:mm:ss and optional *
   * fractional seconds that fit within the 68 char FITS limit            */
  cpl_ensure(strlen(aDateObs) >= 19 && strlen(aDateObs) <= 68,
             CPL_ERROR_ILLEGAL_INPUT, NULL);

  int ioff, noff = cpl_table_get_nrow(aOffsets);
  for (ioff = 0; ioff < noff; ioff++) {
    const char *dateobs = cpl_table_get_string(aOffsets, MUSE_OFFSETS_DATEOBS,
                                               ioff);
    /* compare DATE-OBS to the 23-char limit that's used in *
     * normal ESO DATE keywords, that include milliseconds  */
    if (dateobs && !strncmp(dateobs, aDateObs, 23)) {
      double *offsets = cpl_calloc(3, sizeof(double));
#if 0
      cpl_msg_debug(__func__, "found:");
      cpl_table_dump(aOffsets, ioff, 1, stdout);
      fflush(stdout);
#endif
      int err;
      offsets[0] = cpl_table_get_double(aOffsets, MUSE_OFFSETS_DRA, ioff, &err);
      if (err) {
        cpl_msg_warning(id, "%s for %s could not be read from %s!",
                        MUSE_OFFSETS_DRA, aDateObs, MUSE_TAG_OFFSET_LIST);
        offsets[0] = NAN;
      }
      offsets[1] = cpl_table_get_double(aOffsets, MUSE_OFFSETS_DDEC, ioff, &err);
      if (err) {
        cpl_msg_warning(id, "%s for %s could not be read from %s!",
                        MUSE_OFFSETS_DDEC, aDateObs, MUSE_TAG_OFFSET_LIST);
        offsets[1] = NAN;
      }
      offsets[2] = cpl_table_has_column(aOffsets, MUSE_OFFSETS_FSCALE)
                 ? cpl_table_get_double(aOffsets, MUSE_OFFSETS_FSCALE, ioff, &err)
                 : NAN; /* no scaling by default */
      if (err) {
        offsets[2] = NAN; /* fall back to no scaling */
      }
      return offsets;
    }
  } /* for ioff (all offset table rows) */
  return NULL;
} /* muse_xcombine_find_offsets() */

/*----------------------------------------------------------------------------*/
/**
  @brief   combine the pixel tables of several exposures into one
  @param   aPixtables   the NULL-terminated array of pixel tables to combine
  @param   aOffsets     a table with coordinate offsets to apply (optional, can
                        be NULL)
  @return  a muse_pixtable * with the combined pixel table of all exposures
           or NULL on error
  @remark  The FITS headers as passed to this function through the
           muse_pixtable objects are assumed to contain accurate on-sky
           positional information (RA and DEC).
  @remark  The input pixel tables needs to contain a column with weights;
           otherwise all exposures will be weighted equally when resampling
           later.
  @remark  To get an actual combined datacube, run one of the
           muse_resampling_<format> functions on the output data of this
           function.
  @warning Do not re-order the output table, otherwise the exposure ranges in
           the pixel table headers are out of sync!
  @remark  This function adds a FITS header (@ref MUSE_HDR_PT_COMBINED) with
           the number of combined exposures, for information.

  Determine offsets from input FITS headers, loop through all pixel tables,
  position them to the RA,DEC in their headers using @ref
  muse_wcs_position_celestial(), and append the small pixel tables to the end
  of the output pixel table.
  All used input pixel tables are deleted in this function, only the input
  pointer remains.

  The behavior of this function can be tweaked using the second argument, a
  table listing offsets in RA and DEC for a given DATE-OBS. This can be used to
  align exposures with different relative offsets not reflected in the RA and
  DEC headers. If this is done, the header keywords MUSE_HDR_OFFSETi_DATEOBS,
  MUSE_HDR_OFFSETi_DRA, and MUSE_HDR_OFFSETi_DDEC are written to the header of
  the output pixel table, to show which RA and DEC offsets were applied to the
  exposure of which DATE-OBS.

  Similarly, if aOffsets contains a column MUSE_OFFSETS_FSCALE, the values are
  interpreted as scaling factors to apply to the given exposure before inserting
  into the big pixel table. If flux scales are applied, they are stored in a
  keyword MUSE_HDR_FLUX_SCALEi.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL, aPixtables is NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         there are less than 2 input pixel tables}
  @error{set CPL_ERROR_INCOMPATIBLE_INPUT\, return NULL,
         first pixel table was not projected to native spherical coordinates}
  @error{output warning, first pixel table was not radial-velocity corrected}
  @error{skip this pixel table,
         other pixel tables were not projected to native spherical coordinates}
  @error{output warning, another pixel table was not radial-velocity corrected}
 */
/*----------------------------------------------------------------------------*/
muse_pixtable *
muse_xcombine_tables(muse_pixtable **aPixtables, const cpl_table *aOffsets)
{
  cpl_ensure(aPixtables, CPL_ERROR_NULL_INPUT, NULL);
  unsigned int npt = 0;
  while (aPixtables[npt++]) ; /* count tables, including first NULL table */
  cpl_ensure(--npt > 1, CPL_ERROR_ILLEGAL_INPUT, NULL); /* subtract NULL table */
  cpl_ensure(muse_pixtable_wcs_check(aPixtables[0]) == MUSE_PIXTABLE_WCS_NATSPH,
             CPL_ERROR_INCOMPATIBLE_INPUT, NULL);
  cpl_msg_info(__func__, "%u tables to be combined", npt);

  double timeinit = cpl_test_get_walltime(),
         cpuinit = cpl_test_get_cputime();
  muse_utils_memory_dump("muse_xcombine_tables() start");
  muse_pixtable *pt = aPixtables[0];
  aPixtables[0] = NULL;
  /* warn, if the table was not RV corrected */
  if (!muse_pixtable_is_rvcorr(pt)) {
    cpl_msg_warning(__func__, "Data of exposure 1 (DATE-OBS=%s) was not radial-"
                    "velocity corrected!", muse_pfits_get_dateobs(pt->header));
  }
  /* change exposure number in offset keywords to 1 */
  muse_pixtable_origin_copy_offsets(pt, NULL, 1);
  /* add exposure range */
  char keyword[KEYWORD_LENGTH], comment[KEYWORD_LENGTH];
  snprintf(keyword, KEYWORD_LENGTH, MUSE_HDR_PT_EXP_FST, 1);
  cpl_propertylist_append_long_long(pt->header, keyword, 0);
  snprintf(comment, KEYWORD_LENGTH, MUSE_HDR_PT_EXP_FST_COMMENT, 1);
  cpl_propertylist_set_comment(pt->header, keyword, comment);
  snprintf(keyword, KEYWORD_LENGTH, MUSE_HDR_PT_EXP_LST, 1);
  cpl_propertylist_append_long_long(pt->header, keyword,
                                    muse_pixtable_get_nrow(pt) - 1);
  snprintf(comment, KEYWORD_LENGTH, MUSE_HDR_PT_EXP_LST_COMMENT, 1);
  cpl_propertylist_set_comment(pt->header, keyword, comment);

  double ra0 =  muse_pfits_get_ra(pt->header),
         dec0 = muse_pfits_get_dec(pt->header),
         *offsets = aOffsets
                  ? muse_xcombine_find_offsets(aOffsets,
                                               muse_pfits_get_dateobs(pt->header))
                  : NULL;
  if (offsets) {
    if (isfinite(offsets[0]) && isfinite(offsets[1])) {
      ra0 -= offsets[0];
      dec0 -= offsets[1];
      cpl_msg_debug(__func__, "Applying coordinate offsets to exposure 1: %e/%e"
                    " deg", offsets[0], offsets[1]);
      /* store in the header of the output pixel table */
      snprintf(keyword, KEYWORD_LENGTH, MUSE_HDR_OFFSETi_DRA, 1);
      snprintf(comment, KEYWORD_LENGTH, MUSE_HDR_OFFSETi_DRA_C, offsets[0] * 3600.);
      cpl_propertylist_append_double(pt->header, keyword, offsets[0]);
      cpl_propertylist_set_comment(pt->header, keyword, comment);
      snprintf(keyword, KEYWORD_LENGTH, MUSE_HDR_OFFSETi_DDEC, 1);
      snprintf(comment, KEYWORD_LENGTH, MUSE_HDR_OFFSETi_DDEC_C, offsets[1] * 3600.);
      cpl_propertylist_append_double(pt->header, keyword, offsets[1]);
      cpl_propertylist_set_comment(pt->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 1 by %g.", offsets[2]);
      muse_pixtable_flux_multiply(pt, offsets[2]);
      /* store in the header of the output pixel table */
      snprintf(keyword, KEYWORD_LENGTH, MUSE_HDR_FLUX_SCALEi, 1);
      cpl_propertylist_append_double(pt->header, keyword, offsets[2]);
      cpl_propertylist_set_comment(pt->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, 1);
    snprintf(comment, KEYWORD_LENGTH, MUSE_HDR_OFFSETi_DATEOBS_C, 1);
    cpl_propertylist_append_string(pt->header, keyword,
                                   muse_pfits_get_dateobs(pt->header));
    cpl_propertylist_set_comment(pt->header, keyword, comment);
  } /* if offsets */
  cpl_free(offsets);
  muse_wcs_position_celestial(pt, ra0, dec0);

  unsigned int i, nskipped = 0;
  for (i = 1; i < npt; i++) {
    if (muse_pixtable_wcs_check(aPixtables[i]) != MUSE_PIXTABLE_WCS_NATSPH) {
      cpl_msg_warning(__func__, "Exposure %d was not projected to native "
                      "spherical coordinates, skipping this one!", i + 1);
      nskipped++;
      continue;
    }
    if (!muse_pixtable_is_rvcorr(pt)) {
      cpl_msg_warning(__func__, "Data of exposure %u (DATE-OBS=%s) was not "
                      "radial-velocity corrected!", i+1,
                      muse_pfits_get_dateobs(aPixtables[i]->header));
    }

    /* apply spherical coordinate rotation to coordinates of this exposure */
    double ra = muse_pfits_get_ra(aPixtables[i]->header),
           dec = muse_pfits_get_dec(aPixtables[i]->header);
    offsets = aOffsets
            ? muse_xcombine_find_offsets(aOffsets,
                                         muse_pfits_get_dateobs(aPixtables[i]->header))
            : NULL;
    cpl_boolean offcor = CPL_FALSE; /* if any offset correction was applied */
    if (offsets) {
      if (isfinite(offsets[0]) && isfinite(offsets[1])) {
        ra -= offsets[0];
        dec -= offsets[1];
        cpl_msg_debug(__func__, "Applying coordinate offsets to exposure %d: "
                      "%e/%e deg", i + 1, offsets[0], offsets[1]);
        offcor = CPL_TRUE;
        /* store in the header of the output pixel table */
        snprintf(keyword, KEYWORD_LENGTH, MUSE_HDR_OFFSETi_DRA, i + 1);
        snprintf(comment, KEYWORD_LENGTH, MUSE_HDR_OFFSETi_DRA_C, offsets[0] * 3600.);
        cpl_propertylist_append_double(pt->header, keyword, offsets[0]);
        cpl_propertylist_set_comment(pt->header, keyword, comment);
        snprintf(keyword, KEYWORD_LENGTH, MUSE_HDR_OFFSETi_DDEC, i + 1);
        snprintf(comment, KEYWORD_LENGTH, MUSE_HDR_OFFSETi_DDEC_C, offsets[1] * 3600.);
        cpl_propertylist_append_double(pt->header, keyword, offsets[1]);
        cpl_propertylist_set_comment(pt->header, keyword, comment);
      } /* if spatial offsets */
      if (isnormal(offsets[2])) { /* valid flux scale */
        cpl_msg_debug(__func__, "Scaling flux of exposure %u by %g.", i + 1,
                      offsets[2]);
        muse_pixtable_flux_multiply(aPixtables[i], offsets[2]);
        /* store in the header of the output pixel table */
        snprintf(keyword, KEYWORD_LENGTH, MUSE_HDR_FLUX_SCALEi, i + 1);
        cpl_propertylist_append_double(pt->header, keyword, offsets[2]);
        cpl_propertylist_set_comment(pt->header, keyword, MUSE_HDR_FLUX_SCALEi_C);
      } /* if scale */
      /* store in the header of the output pixel table */
      snprintf(keyword, KEYWORD_LENGTH, MUSE_HDR_OFFSETi_DATEOBS, i + 1);
      snprintf(comment, KEYWORD_LENGTH, MUSE_HDR_OFFSETi_DATEOBS_C, i + 1);
      cpl_propertylist_append_string(pt->header, keyword,
                                     muse_pfits_get_dateobs(aPixtables[i]->header));
      cpl_propertylist_set_comment(pt->header, keyword, comment);
    } /* if offsets */
    cpl_free(offsets);
    muse_wcs_position_celestial(aPixtables[i], ra, dec);

    /* Shift the x/y coordinates depending on their relative zeropoint! */
    double raoffset = ra - ra0,
           decoffset = dec - dec0;
#if 0
    /* this is not switched on, since it actually degrades performance by *
     * 10% compared to the state before, without this offset correction   */
    float *xpos = cpl_table_get_data_float(aPixtables[i]->table, MUSE_PIXTABLE_XPOS),
          *ypos = cpl_table_get_data_float(aPixtables[i]->table, MUSE_PIXTABLE_YPOS);
    cpl_size irow, nrowi = muse_pixtable_get_nrow(aPixtables[i]);
    #pragma omp parallel for default(none)               /* as req. by Ralf */ \
            shared(decoffset, nrowi, raoffset, xpos, ypos)
    for (irow = 0; irow < nrowi; irow++) {
      xpos[irow] += raoffset;
      ypos[irow] += decoffset;
    } /* for irow */
#else
    /* using these functions, the speed degradation is not measurable */
    cpl_table_add_scalar(aPixtables[i]->table, MUSE_PIXTABLE_XPOS, raoffset);
    cpl_table_add_scalar(aPixtables[i]->table, MUSE_PIXTABLE_YPOS, decoffset);
#endif

    /* compute simple (inaccurate!) offset for information */
    double avdec = (dec + dec0) / 2.,
           raoff = (ra - ra0) * cos(avdec * CPL_MATH_RAD_DEG) * 3600.,
           decoff = (dec - dec0) * 3600.;
    cpl_msg_info(__func__, "Distance of exposure %u (relative to exp. 1): "
                 "%.1f,%.1f arcsec%s", i+1, raoff, decoff,
                 offcor ? " (corrected offset)" : "");

    /* append the next pixel table to the end and delete the original */
    cpl_size nrow = muse_pixtable_get_nrow(pt);
    cpl_table_insert(pt->table, aPixtables[i]->table, nrow);
    /* copy the offset headers to the output pixel table before deleting it */
    muse_pixtable_origin_copy_offsets(pt, aPixtables[i], i + 1);
    muse_pixtable_delete(aPixtables[i]);
    aPixtables[i] = NULL;

    /* add respective exposure range to the header */
    snprintf(keyword, KEYWORD_LENGTH, MUSE_HDR_PT_EXP_FST, i + 1);
    cpl_propertylist_append_long_long(pt->header, keyword, nrow);
    snprintf(comment, KEYWORD_LENGTH, MUSE_HDR_PT_EXP_FST_COMMENT, i + 1);
    cpl_propertylist_set_comment(pt->header, keyword, comment);
    snprintf(keyword, KEYWORD_LENGTH, MUSE_HDR_PT_EXP_LST, i + 1);
    cpl_propertylist_append_long_long(pt->header, keyword,
                                      muse_pixtable_get_nrow(pt) - 1);
    snprintf(comment, KEYWORD_LENGTH, MUSE_HDR_PT_EXP_LST_COMMENT, i + 1);
    cpl_propertylist_set_comment(pt->header, keyword, comment);
  } /* for i (pixel tables) */
  muse_pixtable_compute_limits(pt);
  /* update the merge-related status header */
  cpl_propertylist_update_int(pt->header, MUSE_HDR_PT_COMBINED, npt - nskipped);
  cpl_propertylist_set_comment(pt->header, MUSE_HDR_PT_COMBINED,
                               MUSE_HDR_PT_COMBINED_COMMENT);
  /* debug timing */
  double timefini = cpl_test_get_walltime(),
         cpufini = cpl_test_get_cputime();
  muse_utils_memory_dump("muse_xcombine_tables() end");
  cpl_msg_debug(__func__, "Combining %u tables took %gs (wall-clock) and %gs "
                "(CPU)", npt, timefini - timeinit, cpufini - cpuinit);
  return pt;
} /* muse_xcombine_tables() */

/**@}*/
