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

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

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

#include <muse.h>
#include "muse_scipost_z.h"

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

/*----------------------------------------------------------------------------*/
/**
  @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,autocal,raman,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 */
  prop->astrometry = aParams->astrometry;
  if (aParams->astrometry) {
    prop->wcs = muse_processing_load_header(aProcessing, MUSE_TAG_ASTROMETRY_WCS);
  }
  /* self calibration */
  prop->autocalib = MUSE_POSTPROC_AUTOCALIB_NONE;
  if (aParams->autocalib == MUSE_SCIPOST_PARAM_AUTOCALIB_DEEPFIELD) {
    prop->autocalib = MUSE_POSTPROC_AUTOCALIB_DEEPFIELD;
  } else if (aParams->autocalib == MUSE_SCIPOST_PARAM_AUTOCALIB_USER) {
    prop->autocalib = MUSE_POSTPROC_AUTOCALIB_USER;
    prop->autocal_table = muse_processing_load_table(aProcessing,
                                                     MUSE_TAG_ACAL_FACTORS, 0);
    if (!prop->autocal_table) {
      /* autocalib=user but no table given */
      cpl_msg_warning(__func__, "User-provided %s could not be loaded, will "
                      "create it from the data!", MUSE_TAG_ACAL_FACTORS);
    }
  }

  /* Raman emission handling */
  prop->raman_width = aParams->raman_width;
  prop->raman_lines = muse_raman_lines_load(aProcessing);
  /* sky subtraction methods and parameters */
  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);

  /* further setup for sky subtraction */
#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);
  } else if (aParams->autocalib == MUSE_SCIPOST_PARAM_AUTOCALIB_DEEPFIELD) {
    /* the sky mask is still used in case of autocalibration */
    prop->sky_mask = muse_processing_load_mask(aProcessing, MUSE_TAG_SKY_MASK);
  }

  /* warn about bad combinations of inputs regarding Raman correction */
  if (prop->raman_lines) {
    cpl_msg_debug(__func__, "Raman 2D correction wanted (%s given)...",
                  MUSE_TAG_RAMAN_LINES);
    if (prop->sky_continuum) {
      cpl_msg_warning(__func__, "Using a %s together with Raman correction (%s)"
                      " is unlikely to give good results!", MUSE_TAG_SKY_CONT,
                      MUSE_TAG_RAMAN_LINES);
    }
    if (prop->skymodel_params.fraction < 0.5) {
      cpl_msg_warning(__func__, "Using a small sky fraction (%.2f) together "
                      "with Raman correction (%s) is unlikely to give good "
                      "results!", prop->skymodel_params.fraction,
                      MUSE_TAG_RAMAN_LINES);
    }
  }

  /* also load the LSF, if we need it for Raman correction */
  if (prop->raman_lines && !prop->lsf_cube) {
    prop->lsf_cube = muse_lsf_cube_load_all(aProcessing);
  }

  /* 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_autocal_outputs *autocal
      = cpl_calloc(1, sizeof(muse_postproc_autocal_outputs));
    muse_datacube *ramancube = NULL;
    muse_postproc_sky_outputs *skyout
      = cpl_calloc(1, sizeof(muse_postproc_sky_outputs));
    pixtables[i] = muse_postproc_process_exposure(prop, i, autocal, &ramancube,
                                                  skyout, offsets);
    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);
      muse_mask_delete(autocal->mask);
      muse_table_delete(autocal->table);
      cpl_free(autocal);
      muse_datacube_delete(ramancube);
      muse_image_delete(skyout->image);
      muse_mask_delete(skyout->mask);
      cpl_table_delete(skyout->spectrum);
      cpl_table_delete(skyout->lines);
      cpl_table_delete(skyout->continuum);
      cpl_free(skyout);
      return -1; /* enough error messages, just return */
    } /* if !pixtable */

    /* save the autocalibration products, if requested */
    if (strstr(aParams->save, "autocal")) {
      if (autocal->mask) {
        muse_processing_save_mask(aProcessing, -1, autocal->mask,
                                  MUSE_TAG_ACAL_MASK);
      }
      if (autocal->table) {
        muse_processing_save_table(aProcessing, -1, autocal->table, NULL,
                                   MUSE_TAG_ACAL_FACTORS, MUSE_TABLE_TYPE_MUSE);
      }
    } /* if save autocal */
    muse_mask_delete(autocal->mask);
    muse_table_delete(autocal->table);
    cpl_free(autocal);

    /* save the Raman product, if requested */
    if (strstr(aParams->save, "raman") && ramancube) {
      muse_processing_save_cube(aProcessing, -1, ramancube,
                                MUSE_TAG_RAMAN_IMAGES, MUSE_CUBE_TYPE_FITS);
      muse_datacube_delete(ramancube);
    } /* if save Raman outputs */

    /* save the effectively used sky products, if requested */
    if (strstr(aParams->save, "skymodel")) {
      if (skyout->image) {
        muse_processing_save_image(aProcessing, -1, skyout->image, MUSE_TAG_SKY_IMAGE);
      }
      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);
      }
    } /* if save skymodel */
    muse_image_delete(skyout->image);
    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_postproc_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_postproc_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;
    muse_resampling_params_set_pixfrac(rp, 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() */
