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

#include "muse_sky.h"
#include "muse_cplwrappers.h"
#include "muse_data_format_z.h"

/*----------------------------------------------------------------------------*/
/**
 * @defgroup muse_skysub       Sky subtraction
 *
 * The sky subtraction consists of two steps: the calculation of the common
 * sky parameters (continuum spectrum and emission line fluxes),
 * and the actual subtraction of this spectrum applied to LSF parameters
 * from each slice. The communication between these two steps is realized with
 * the <tt>muse_sky_master</tt> structure.
 *
 * As input for the master sky calculation a parameter file is used that
 * will be read into a <tt>muse_sky_lines</tt> structure.
 *
 * @copydetails muse_sky_lines_oh_transitions_def
 *
 * @copydetails muse_sky_lines_lines_def
 */
/*----------------------------------------------------------------------------*/
/**@{*/

/*----------------------------------------------------------------------------*/
/**
   @brief Create spectrum for a single slice.
   @param aLambda     Wavelength array.
   @param aLines      Sky emission line table
   @param aLsfImage   LSF data to use
   @param aLsfWCS     WCS information for the LSF data
   return the spectrum as cpl_array

   The values in the returned array correspond to the wavelengths in the
   aLambda parameter.
 */
/*----------------------------------------------------------------------------*/
cpl_array *
muse_sky_lines_spectrum(const cpl_array *aLambda, cpl_table *aLines,
                        const cpl_image *aLsfImage,
                        const muse_wcs *aLsfWCS)
{
  cpl_size n_lines = cpl_table_get_nrow(aLines);
  cpl_size i_line;
  double l_min = aLsfWCS->crval1 + aLsfWCS->cd11 * (1 - aLsfWCS->crpix1),
         l_max = aLsfWCS->crval1
               + aLsfWCS->cd11 * (cpl_image_get_size_x(aLsfImage) - aLsfWCS->crpix1);
  cpl_array *spectrum = cpl_array_new(cpl_array_get_size(aLambda),
                                      CPL_TYPE_DOUBLE);
  cpl_array_fill_window(spectrum, 0, cpl_array_get_size(aLambda), 0.0);
  for (i_line = 0; i_line < n_lines; i_line++) {
    double l_lambda = cpl_table_get(aLines, "lambda", i_line, NULL);
    double l_flux = cpl_table_get(aLines, "flux", i_line, NULL);
    cpl_size imin = muse_cplarray_find_sorted(aLambda, l_lambda + l_min);
    cpl_size imax = muse_cplarray_find_sorted(aLambda, l_lambda + l_max);
    if (imax <= imin) {
      continue;
    }
    cpl_array *l0 = cpl_array_extract(aLambda, imin, imax-imin+1);
    cpl_array_subtract_scalar(l0, l_lambda);
    muse_lsf_apply(aLsfImage, aLsfWCS, l0, l_lambda);
    cpl_array_multiply_scalar(l0, l_flux);
    muse_cplarray_add_window(spectrum, imin, l0);
    cpl_array_delete(l0);
  }
  return spectrum;
}

/*----------------------------------------------------------------------------*/
/**
   @private
   Subtract one the lines from one individual slice (one LSF) from the
   pixel table.
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
muse_sky_subtract_lines_slice(muse_pixtable *aPixtable, cpl_table *aLines,
                              cpl_image *aLsfImage, muse_wcs *aLsfWCS)
{
  cpl_propertylist *order = cpl_propertylist_new();
  cpl_propertylist_append_bool(order, MUSE_PIXTABLE_LAMBDA, CPL_FALSE);
  cpl_table_sort(aPixtable->table, order);
  cpl_propertylist_delete(order);

  cpl_array *data = muse_cpltable_extract_column(aPixtable->table,
                                                 MUSE_PIXTABLE_DATA);

  cpl_array *lambda = NULL;
  if (cpl_table_get_column_type(aPixtable->table, MUSE_PIXTABLE_LAMBDA)
      == CPL_TYPE_DOUBLE) {
    lambda = muse_cpltable_extract_column(aPixtable->table,
                                          MUSE_PIXTABLE_LAMBDA);
  } else {
    cpl_table_cast_column(aPixtable->table, MUSE_PIXTABLE_LAMBDA,
                          "lambda_double", CPL_TYPE_DOUBLE);
    lambda = muse_cpltable_extract_column(aPixtable->table, "lambda_double");
  }

  if ((aLines != NULL) && (aLsfImage != NULL) && (aLsfWCS != NULL)) {
    cpl_array *spectrum = muse_sky_lines_spectrum(lambda, aLines,
                                                  aLsfImage, aLsfWCS);
    cpl_array_subtract(data, spectrum);
    cpl_array_delete(spectrum);
  }

  cpl_array_unwrap(data);
  cpl_array_unwrap(lambda);
  if (cpl_table_has_column(aPixtable->table, "lambda_double")) {
    cpl_table_erase_column(aPixtable->table, "lambda_double");
  }

  return CPL_ERROR_NONE;
}

/*----------------------------------------------------------------------------*/
/**
   @brief Subtract sky lines from a pixtable.
   @param aPixtable   Pixel table. The <tt>data</tt> column will be changed.
   @param aLines      Line list with individual line fluxes
   @param aLsfCube    The LSF cube
   @return CPL_ERROR_NONE on success or another CPL error code on failure
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_sky_subtract_lines(muse_pixtable *aPixtable,
                        cpl_table *aLines, muse_lsf_cube **aLsfCube)
{
  cpl_ensure_code(aPixtable != NULL, CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(aPixtable->table != NULL, CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(muse_cpltable_check(aPixtable->table, muse_pixtable_def) == CPL_ERROR_NONE,
                  CPL_ERROR_DATA_NOT_FOUND);
  cpl_ensure_code(aLines != NULL, CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(aLsfCube != NULL, CPL_ERROR_NULL_INPUT);

  muse_pixtable **slice_pixtable =
    muse_pixtable_extracted_get_slices(aPixtable);
  cpl_size n_slices = muse_pixtable_extracted_get_size(slice_pixtable);
  cpl_size i_slice;
  cpl_error_code rc[n_slices];
  cpl_msg_info(__func__, "Starting sky subtraction of %"CPL_SIZE_FORMAT
               " slices", n_slices);
  cpl_boolean debug = getenv("MUSE_DEBUG_SKY")
                    && atoi(getenv("MUSE_DEBUG_SKY")) > 0;
  #pragma omp parallel for default(none)                                       \
          shared(aLsfCube, aLines, debug, n_slices, slice_pixtable, rc)
  for (i_slice = 0; i_slice < n_slices; i_slice++) {
    muse_pixtable *slice_pt = slice_pixtable[i_slice];
      uint32_t origin =
      (uint32_t) cpl_table_get_int(slice_pt->table,
                                   MUSE_PIXTABLE_ORIGIN, 0, NULL);
    int ifu = muse_pixtable_origin_get_ifu(origin);
    int slice = muse_pixtable_origin_get_slice(origin);
    if ((aLsfCube[ifu-1] == NULL) && (aLines != NULL)) {
      cpl_msg_warning(__func__,
                      "No LSF params for slice #%i.%i."
                      " Ignoring lines in sky subtraction for this slice.",
                      ifu, slice);
      rc[i_slice] = CPL_ERROR_NONE;
      continue;
    }

    cpl_size nrows = muse_pixtable_get_nrow(slice_pt);
    if (debug) {
      cpl_msg_debug(__func__, "Sky subtraction of %"CPL_SIZE_FORMAT" pixels for"
                    " slice #%i.%i (%i)", nrows, ifu, slice, (int)i_slice+1);
    }
    cpl_image *lsfImage = cpl_imagelist_get(aLsfCube[ifu-1]->img, slice-1);
    rc[i_slice] = muse_sky_subtract_lines_slice(slice_pt,
                                                aLines, lsfImage,
                                                aLsfCube[ifu-1]->wcs);
  }
  muse_pixtable_extracted_delete(slice_pixtable);

  for (i_slice = 0; i_slice < n_slices; i_slice++) {
    if (rc[i_slice] != CPL_ERROR_NONE) {
      return rc[i_slice];
    }
  }
  return CPL_ERROR_NONE;
}

/*----------------------------------------------------------------------------*/
/**
   @brief Subtract sky continuum from pixel table.
   @param aPixtable Pixel table. The <tt>data</tt> column will be changed.
   @param aSpectrum Continuum spectrum
   @return CPL_ERROR_NONE on success or another CPL error code on failure
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_sky_subtract_continuum(muse_pixtable *aPixtable, cpl_table *aSpectrum)
{
  cpl_ensure_code(aPixtable != NULL, CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(aPixtable->table != NULL, CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(muse_cpltable_check(aPixtable->table, muse_pixtable_def)
                  == CPL_ERROR_NONE, CPL_ERROR_DATA_NOT_FOUND);
  cpl_ensure_code(aSpectrum != NULL, CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(cpl_table_has_column(aSpectrum, "lambda") &&
                  cpl_table_has_column(aSpectrum, "flux"),
                  CPL_ERROR_DATA_NOT_FOUND);

  /* cut input pixel table to the length of the continuum in the sky master */
  double lmin = cpl_table_get_column_min(aSpectrum, "lambda");
  double lmax = cpl_table_get_column_max(aSpectrum, "lambda");
  cpl_msg_info(__func__, "Cutting data to %.3f...%.3f Angstrom for sky "
               "subtraction (range of continuum)", lmin, lmax);
  muse_pixtable_restrict_wavelength(aPixtable, lmin, lmax);

  cpl_array *lambda = muse_cpltable_extract_column(aSpectrum, "lambda"),
            *flux = muse_cpltable_extract_column(aSpectrum, "flux");
  muse_pixtable_spectrum_apply(aPixtable, lambda, flux,
                               MUSE_PIXTABLE_OPERATION_SUBTRACT);
  cpl_array_unwrap(lambda);
  cpl_array_unwrap(flux);

  return CPL_ERROR_NONE;
} /* muse_sky_subtract_continuum() */

/**@}*/
