/* -*- 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 <muse.h>
#include "muse_scipost_z.h"

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

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

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

/*----------------------------------------------------------------------------*/
/**
  @brief    Interpret the command line options and execute the data processing
  @param    aProcessing  the processing structure
  @param    aParams      the parameters list
  @return   0 if everything is ok, -1 something went wrong

  @pseudocode
  muse_processing_sort_exposures()
  for i = 0 ... nexposures:
    muse_postproc_process_exposure()
  if nexposures > 1:
    muse_xcombine_weights()
    muse_xcombine_tables()
  muse_resampling_cube() or muse_resampling_euro3d()@endpseudocode
 */
/*----------------------------------------------------------------------------*/
int
muse_scipost_compute(muse_processing *aProcessing,
                     muse_scipost_params_t *aParams)
{
  const char *savevalid = "cube,skymodel,individual,positioned,combined,stacked";
  if (!muse_postproc_check_save_param(aParams->save, savevalid)) {
    return -1;
  }
  muse_postproc_properties *prop = muse_postproc_properties_new(MUSE_POSTPROC_SCIPOST);
  /* per-exposure parameters */
  prop->lambdamin = aParams->lambdamin;
  prop->lambdamax = aParams->lambdamax;
  prop->lambdaref = aParams->lambdaref;
  prop->darcheck = MUSE_POSTPROC_DARCHECK_NONE;
  if (aParams->darcheck == MUSE_SCIPOST_PARAM_DARCHECK_CHECK) {
    prop->darcheck = MUSE_POSTPROC_DARCHECK_CHECK;
  } else if (aParams->darcheck == MUSE_SCIPOST_PARAM_DARCHECK_CORRECT) {
    prop->darcheck = MUSE_POSTPROC_DARCHECK_CORRECT;
  }
  prop->rvtype = muse_rvcorrect_select_type(aParams->rvcorr_s);
  /* flux calibration */
  prop->response = muse_processing_load_table(aProcessing,
                                              MUSE_TAG_STD_RESPONSE, 0);
  prop->telluric = muse_processing_load_table(aProcessing,
                                              MUSE_TAG_STD_TELLURIC, 0);
  prop->extinction = muse_processing_load_ctable(aProcessing,
                                                 MUSE_TAG_EXTINCT_TABLE, 0);
  /* astrometric correction */
  if (aParams->astrometry) {
    prop->wcs = muse_processing_load_header(aProcessing, MUSE_TAG_ASTROMETRY_WCS);
    if (!prop->wcs) {
      /* if it was not in the input, create a default WFM astrometry */
      cpl_msg_warning(__func__, "Using default MUSE WFM astrometry, output "
                      "world coordinates will be inaccurate!");
      prop->wcs = muse_wcs_create_default();
    }
  }

  const struct {
    int par;
    muse_postproc_skymethod postproc;
  } skymethod[] = {
    { MUSE_SCIPOST_PARAM_SKYMETHOD_NONE, MUSE_POSTPROC_SKYMETHOD_NONE },
    { MUSE_SCIPOST_PARAM_SKYMETHOD_SUBTRACT_MODEL, MUSE_POSTPROC_SKYMETHOD_NONE }, /* sic! */
    { MUSE_SCIPOST_PARAM_SKYMETHOD_MODEL, MUSE_POSTPROC_SKYMETHOD_MODEL },
    { MUSE_SCIPOST_PARAM_SKYMETHOD_SIMPLE, MUSE_POSTPROC_SKYMETHOD_SIMPLE },
    { MUSE_SCIPOST_PARAM_SKYMETHOD_INVALID_VALUE, MUSE_POSTPROC_SKYMETHOD_MODEL },
  };
  prop->skymethod = MUSE_POSTPROC_SKYMETHOD_MODEL;
  int i_method;
  for (i_method = 0;
       skymethod[i_method].par != MUSE_SCIPOST_PARAM_SKYMETHOD_INVALID_VALUE;
       i_method++) {
    if (skymethod[i_method].par == aParams->skymethod) {
      prop->skymethod = skymethod[i_method].postproc;
      break;
    }
  }
  prop->skymodel_params.fraction = aParams->skymodel_fraction;
  prop->skymodel_params.ignore = aParams->skymodel_ignore;
  prop->skymodel_params.sampling = aParams->skymodel_sampling;
  prop->skymodel_params.csampling = aParams->skymodel_csampling;
  /* CR rejection sigmas */
  cpl_array *crarray = muse_cplarray_new_from_delimited_string(aParams->sky_crsigma, ",");
  if (cpl_array_get_size(crarray) < 2) {
    prop->skymodel_params.crsigmac = 15.;
    prop->skymodel_params.crsigmas = 0.;
    cpl_msg_warning(__func__, "Less than two values given by sky_crsigma "
                    "parameter, using defaults (%.3f.,%.3f)!",
                    prop->skymodel_params.crsigmac, prop->skymodel_params.crsigmas);
  } else {
    prop->skymodel_params.crsigmac = cpl_array_get_string(crarray, 0)
                                   ? atof(cpl_array_get_string(crarray, 0)) : 15.;
    prop->skymodel_params.crsigmas = cpl_array_get_string(crarray, 1)
                                   ? atof(cpl_array_get_string(crarray, 1)) : 0.;
  }
  cpl_array_delete(crarray);

#ifdef USE_LSF_PARAMS
  cpl_errorstate prestate = cpl_errorstate_get();
#endif
  if (aParams->skymethod == MUSE_SCIPOST_PARAM_SKYMETHOD_SUBTRACT_MODEL) {
    prop->lsf_cube = muse_lsf_cube_load_all(aProcessing);
    if (prop->lsf_cube != NULL) {
      prop->sky_lines = muse_sky_lines_load(aProcessing);
#ifdef USE_LSF_PARAMS
    } else {
      prop-> lsf_params = muse_processing_lsf_params_load(aProcessing, 0);
      if (prop->lsf_params != NULL) {  // Old LSF params code
        cpl_errorstate_set(prestate);
        prop->sky_lines = muse_sky_lines_load(aProcessing);
      }
#endif
    }
    prop->sky_continuum = muse_sky_continuum_load(aProcessing);
  } else if (aParams->skymethod == MUSE_SCIPOST_PARAM_SKYMETHOD_MODEL) {
    prop->lsf_cube = muse_lsf_cube_load_all(aProcessing);
    if (prop->lsf_cube == NULL) {
#ifdef USE_LSF_PARAMS
      prop->lsf_params = muse_processing_lsf_params_load(aProcessing, 0);
      if (prop->lsf_params != NULL) {  // Old LSF params code
        cpl_errorstate_set(prestate);
      } else {
#endif
        cpl_msg_error(__func__, "Missing required LSF frames for sky model");
#ifdef USE_LSF_PARAMS
      }
#endif
    }
    prop->sky_lines = muse_sky_lines_load(aProcessing);
    if (prop->sky_lines == NULL) {
      cpl_msg_error(__func__, "Missing required sky lines frame for sky model");
    }
    if (((prop->lsf_cube == NULL)
#ifdef USE_LSF_PARAMS
         && (prop->lsf_params == NULL)
#endif
         )
        || (prop->sky_lines == NULL)) {
      muse_postproc_properties_delete(prop);
      return -1;
    }
    prop->sky_continuum = muse_sky_continuum_load(aProcessing);
    prop->sky_mask = muse_processing_load_mask(aProcessing, MUSE_TAG_SKY_MASK);
  } else if (aParams->skymethod == MUSE_SCIPOST_PARAM_SKYMETHOD_SIMPLE) {
    /* a mask could help for the simple sky subtraction */
    prop->sky_mask = muse_processing_load_mask(aProcessing, MUSE_TAG_SKY_MASK);
  }

  /* sort input pixel tables into different exposures */
  prop->exposures = muse_processing_sort_exposures(aProcessing);
  if (!prop->exposures) {
    cpl_msg_error(__func__, "no science exposures found in input");
    muse_postproc_properties_delete(prop);
    return -1;
  }
  int nexposures = cpl_table_get_nrow(prop->exposures);

  /* load optional offsets */
  cpl_table *offsets = muse_processing_load_ctable(aProcessing, MUSE_TAG_OFFSET_LIST, 0);
  if (offsets && muse_cpltable_check(offsets, muse_offset_list_def) != CPL_ERROR_NONE) {
    cpl_msg_warning(__func__, "Input %s has unexpected format, proceeding "
                    "without offset and flux scales!", MUSE_TAG_OFFSET_LIST);
    cpl_table_delete(offsets);
    offsets = NULL;
  } /* if offsets have wrong format */

  /* now process all the pixel tables, do it separately for each exposure */
  /* allocate one additional element for NULL termination */
  muse_pixtable **pixtables = cpl_calloc(nexposures + 1, sizeof(muse_pixtable *));
  int i;
  for (i = 0; i < nexposures; i++) {
    muse_postproc_sky_outputs *skyout = cpl_calloc(1, sizeof(muse_postproc_sky_outputs));
    pixtables[i] = muse_postproc_process_exposure(prop, i, skyout);
    if (!pixtables[i]) {
      int i2;
      for (i2 = 0; i2 <= i; i2++) {
        muse_pixtable_delete(pixtables[i2]);
      } /* for i2 */
      cpl_free(pixtables);
      muse_postproc_properties_delete(prop);
      cpl_table_delete(offsets);
      return -1; /* enough error messages, just return */
    }
    /* save the effectively used sky products, if requested */
    if (strstr(aParams->save, "skymodel")) {
      if (skyout->mask) {
        muse_processing_save_mask(aProcessing, -1, skyout->mask, MUSE_TAG_SKY_MASK);
      }
      if (skyout->spectrum) {
        cpl_propertylist *header = cpl_propertylist_duplicate(pixtables[i]->header);
        cpl_propertylist_erase_regexp(header, MUSE_HDR_PT_REGEXP, 0);
        muse_processing_save_table(aProcessing, -1, skyout->spectrum, header,
                                   MUSE_TAG_SKY_SPECTRUM, MUSE_TABLE_TYPE_CPL);
        cpl_propertylist_delete(header);
      }
      if (skyout->lines) {
        cpl_propertylist *header = cpl_propertylist_duplicate(pixtables[i]->header);
        cpl_propertylist_erase_regexp(header, MUSE_HDR_PT_REGEXP, 0);
        muse_sky_qc_lines(header, skyout->lines, "ESO QC SCIPOST");
        muse_sky_lines_save(aProcessing, skyout->lines, header);
        cpl_propertylist_delete(header);
      }
      if (skyout->continuum) {
        cpl_propertylist *header = cpl_propertylist_duplicate(pixtables[i]->header);
        cpl_propertylist_erase_regexp(header, MUSE_HDR_PT_REGEXP, 0);
        muse_sky_qc_continuum(header, skyout->continuum, "ESO QC SCIPOST");
        muse_sky_save_continuum(aProcessing, skyout->continuum, header);
        cpl_propertylist_delete(header);
      }
    }
    muse_mask_delete(skyout->mask);
    cpl_table_delete(skyout->spectrum);
    cpl_table_delete(skyout->lines);
    cpl_table_delete(skyout->continuum);
    cpl_free(skyout);

    if (strstr(aParams->save, "individual")) {
      muse_processing_save_table(aProcessing, -1, pixtables[i], NULL,
                                 MUSE_TAG_PIXTABLE_REDUCED,
                                 MUSE_TABLE_TYPE_PIXTABLE);
    }
    if (strstr(aParams->save, "positioned")) {
      /* duplicate table to not mess with the coordinates in the individual *
       * pixel tables, which are still needed when combining the exposures  */
      muse_pixtable *pt = muse_pixtable_duplicate(pixtables[i]);
      /* since after positioning it is too late to *
       * adapt the reference position, do it now   */
      muse_scipost_offsets_scale(pt, offsets,
                                 "positioned pixel table for saving");
      muse_wcs_position_celestial(pt, muse_pfits_get_ra(pt->header),
                                  muse_pfits_get_dec(pt->header));
      muse_processing_save_table(aProcessing, -1, pt, NULL,
                                 MUSE_TAG_PIXTABLE_POSITIONED,
                                 MUSE_TABLE_TYPE_PIXTABLE);
      muse_pixtable_delete(pt);
    }
  } /* for i (exposures) */
  muse_postproc_properties_delete(prop);

  /* now combine the possibly more than one exposures */
  muse_pixtable *bigpixtable = NULL;
  if (nexposures > 1) {
    muse_xcombine_types weight = muse_postproc_get_weight_type(aParams->weight_s);
    cpl_error_code rc = muse_xcombine_weights(pixtables, weight);
    if (rc != CPL_ERROR_NONE) {
      cpl_msg_error(__func__, "weighting the pixel tables didn't work: %s",
                    cpl_error_get_message());
      for (i = 0; i < nexposures; i++) {
        muse_pixtable_delete(pixtables[i]);
      } /* for i (exposures) */
      cpl_free(pixtables);
      cpl_table_delete(offsets);
      return -1;
    }
    /* combine individual pixel tables and delete them */
    bigpixtable = muse_xcombine_tables(pixtables, offsets);
    if (!bigpixtable) {
      cpl_msg_error(__func__, "combining the pixel tables didn't work: %s",
                    cpl_error_get_message());
      for (i = 0; i < nexposures; i++) {
        muse_pixtable_delete(pixtables[i]);
      } /* for i (exposures) */
      cpl_free(pixtables);
      cpl_table_delete(offsets);
      return -1;
    }
  } else {
    bigpixtable = pixtables[0];
    /* if there is only one pixel table, apply the offset for that */
    cpl_msg_indent_more();
    muse_scipost_offsets_scale(bigpixtable, offsets,
                               "single pixel table for resampling");
    cpl_msg_indent_less();
    muse_wcs_position_celestial(bigpixtable,
                                muse_pfits_get_ra(bigpixtable->header),
                                muse_pfits_get_dec(bigpixtable->header));
  }
  cpl_free(pixtables);
  cpl_table_delete(offsets);

  cpl_error_code rc = CPL_ERROR_NONE;
  if (strstr(aParams->save, "cube")) {
    muse_resampling_type resample
      = muse_postproc_get_resampling_type(aParams->resample_s);
    muse_resampling_params *rp = muse_resampling_params_new(resample);
    rp->dx = aParams->dx;
    rp->dy = aParams->dy;
    rp->dlambda = aParams->dlambda;
    rp->crtype = muse_postproc_get_cr_type(aParams->crtype_s);
    rp->crsigma = aParams->crsigma;
    rp->ld = aParams->ld;
    rp->rc = aParams->rc;
    rp->pfx = aParams->pixfrac;
    rp->pfy = aParams->pixfrac;
    rp->pfl = aParams->pixfrac;
    cpl_propertylist *outwcs = muse_postproc_cube_load_output_wcs(aProcessing);
    muse_resampling_params_set_wcs(rp, outwcs);
    cpl_propertylist_delete(outwcs);
    muse_cube_type format = muse_postproc_get_cube_format(aParams->format_s);
    rc = muse_postproc_cube_resample_and_collapse(aProcessing, bigpixtable,
                                                  format, rp, aParams->filter);
    muse_resampling_params_delete(rp);
  } /* if save contains cube */
  if (strstr(aParams->save, "combined")) {
    muse_processing_save_table(aProcessing, -1, bigpixtable, NULL,
                               MUSE_TAG_PIXTABLE_COMBINED,
                               MUSE_TABLE_TYPE_PIXTABLE);
  }
  if (strstr(aParams->save, "stacked")) {
    cpl_msg_debug(__func__, "additional output as column-stacked image");
    muse_image *img = muse_resampling_image(bigpixtable,
                                            MUSE_RESAMPLE_WEIGHTED_RENKA,
                                            aParams->dx, aParams->dlambda);
    muse_processing_save_image(aProcessing, -1, img, MUSE_TAG_OBJECT_RESAMPLED);
    muse_image_delete(img);
  }

  muse_pixtable_delete(bigpixtable);

  /* the cube is normally the main output, check only its return code */
  return rc == CPL_ERROR_NONE ? 0 : -1;
} /* muse_scipost_compute() */
