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

#include "muse_xcombine.h"

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

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

/**@{*/

/*----------------------------------------------------------------------------*/
/**
  @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{FWHM-weights for these tables are set to 1.\, propagate errors\, output warning\, but return CPL_ERROR_NONE,
         the 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);
  }

  cpl_msg_info(__func__, "%d tables to be weighted using %s", npt,
               aWeighting == MUSE_XCOMBINE_EXPTIME ? "EXPTIME"
                                                   : "EXPTIME & FWHM");
  double exptime0 = muse_pfits_get_exptime(aPixtables[0]->header);
  if (exptime0 == 0.0) {
    return cpl_error_set(__func__, CPL_ERROR_INCOMPATIBLE_INPUT);
  }
  double fwhm0 = aWeighting == MUSE_XCOMBINE_FWHM
               ? (muse_pfits_get_fwhm_start(aPixtables[0]->header)
                  + muse_pfits_get_fwhm_end(aPixtables[0]->header)) / 2.
               : 1.;

  /* add and fill the "weight" column in all pixel tables */
  unsigned int i;
  for (i = 0; i < npt; i++) {
    double exptime = muse_pfits_get_exptime(aPixtables[i]->header),
           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 */
    if (aWeighting == MUSE_XCOMBINE_FWHM) {
      cpl_errorstate prestate = cpl_errorstate_get();
      double fwhm = (muse_pfits_get_fwhm_start(aPixtables[i]->header)
                     + muse_pfits_get_fwhm_end(aPixtables[i]->header)) / 2.;
      if (fwhm == 0. || !cpl_errorstate_is_equal(prestate)) {
        cpl_msg_warning(__func__, "No seeing info in table %d. Weighting it "
                        "equal to first table!", i+1);
        fwhm = fwhm0;
      }
      weight *= fwhm0 / fwhm;
    }
    cpl_msg_debug(__func__, "Table %d, weight = %f", i+1, weight);
    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) */

  return CPL_ERROR_NONE;
} /* muse_xcombine_weights() */

/*----------------------------------------------------------------------------*/
/**
  @brief   combine the pixel tables of several exposures into one
  @param   aPixtables   the NULL-terminated array of pixel tables to combine
  @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 environment variables
  MUSE_XCOMBINE_RA_OFFSETS and MUSE_XCOMBINE_DEC_OFFSETS, so that one can 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.

  @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)
{
  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);

  /* check for environment variables with exposure offsets (in deg) */
  cpl_array *dra = NULL,
            *ddec = NULL;
  char *raenv = getenv("MUSE_XCOMBINE_RA_OFFSETS"),
       *decenv = getenv("MUSE_XCOMBINE_DEC_OFFSETS");
  if (raenv) {
    dra = muse_cplarray_new_from_delimited_string(raenv, ",");
    unsigned int nra = cpl_array_get_size(dra);
    if (nra != npt) {
      cpl_msg_warning(__func__, "Found %u RA offsets for %u exposures, not "
                      "using them!", nra, npt);
      cpl_array_delete(dra);
      dra = NULL;
    } else {
      cpl_msg_info(__func__, "Using %u RA offsets", nra);
    }
  }
  if (decenv) {
    ddec = muse_cplarray_new_from_delimited_string(decenv, ",");
    unsigned int ndec = cpl_array_get_size(ddec);
    if (ndec != npt) {
      cpl_msg_warning(__func__, "Found %u DEC offsets for %u exposures, not "
                      "using them!", ndec, npt);
      cpl_array_delete(ddec);
      ddec = NULL;
    } else {
      cpl_msg_info(__func__, "Using %u DEC offsets", ndec);
    }
  }

  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);
  if (dra) {
    double raoff = atof(cpl_array_get_string(dra, 0));
    ra0 -= raoff;
    /* 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, raoff * 3600.);
    cpl_propertylist_append_double(pt->header, keyword, raoff);
    cpl_propertylist_set_comment(pt->header, keyword, comment);
  }
  if (ddec) {
    double decoff = atof(cpl_array_get_string(ddec, 0));
    dec0 -= decoff;
    /* store in the header of the output pixel table */
    snprintf(keyword, KEYWORD_LENGTH, MUSE_HDR_OFFSETi_DDEC, 1);
    snprintf(comment, KEYWORD_LENGTH, MUSE_HDR_OFFSETi_DDEC_C, decoff * 3600.);
    cpl_propertylist_append_double(pt->header, keyword, decoff);
    cpl_propertylist_set_comment(pt->header, keyword, comment);
  }
  if (dra || ddec) {
    /* store 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);
  }
  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 the first exposure */
    double ra = muse_pfits_get_ra(aPixtables[i]->header),
           dec = muse_pfits_get_dec(aPixtables[i]->header);
    if (dra) {
      double raoff = atof(cpl_array_get_string(dra, i));
      ra -= raoff;
      cpl_msg_debug(__func__, "positioning not to RA %f but to %f (dRA = %f "
                    "deg)", ra + raoff, ra, raoff);
      /* 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, raoff * 3600.);
      cpl_propertylist_append_double(pt->header, keyword, raoff);
      cpl_propertylist_set_comment(pt->header, keyword, comment);
    }
    if (ddec) {
      double decoff = atof(cpl_array_get_string(ddec, i));
      dec -= decoff;
      cpl_msg_debug(__func__, "positioning not to DEC %f but to %f (dDEC = %f "
                    "deg)", dec + decoff, dec, decoff);
      /* store in the header of the output pixel table */
      snprintf(keyword, KEYWORD_LENGTH, MUSE_HDR_OFFSETi_DDEC, i + 1);
      snprintf(comment, KEYWORD_LENGTH, MUSE_HDR_OFFSETi_DDEC_C, decoff * 3600.);
      cpl_propertylist_append_double(pt->header, keyword, decoff);
      cpl_propertylist_set_comment(pt->header, keyword, comment);
    }
    if (dra || ddec) {
      /* 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);
    }
    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),
           decoff = dec - dec0;
    cpl_msg_info(__func__, "Approx. offset of exposure %u: %.3e,%.3e deg", i+1,
                 raoff, decoff);

    /* 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) */
  cpl_array_delete(dra);
  cpl_array_delete(ddec);
  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() */

/**@}*/
