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

  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_boolean debug = getenv("MUSE_DEBUG_SKY")
                    && atoi(getenv("MUSE_DEBUG_SKY")) > 0;
  #pragma omp parallel for default(none)                                       \
          shared(aSpectrum, debug, n_slices, slice_pixtable)
  for (i_slice = 0; i_slice < n_slices; i_slice++) {
    muse_pixtable *slice_pt = slice_pixtable[i_slice];
    cpl_propertylist *order = cpl_propertylist_new();
    cpl_propertylist_append_bool(order, MUSE_PIXTABLE_LAMBDA, CPL_FALSE);
    cpl_table_sort(slice_pt->table, order);
    cpl_propertylist_delete(order);
    cpl_array *lambda = NULL;
    if (cpl_table_get_column_type(slice_pt->table, MUSE_PIXTABLE_LAMBDA)
        == CPL_TYPE_DOUBLE) {
      lambda = muse_cpltable_extract_column(slice_pt->table,
                                            MUSE_PIXTABLE_LAMBDA);
    } else {
      cpl_table_cast_column(slice_pt->table, MUSE_PIXTABLE_LAMBDA,
                            "lambda_double", CPL_TYPE_DOUBLE);
      lambda = muse_cpltable_extract_column(slice_pt->table, "lambda_double");
    }

    cpl_array *data = muse_cpltable_extract_column(slice_pt->table,
                                                   MUSE_PIXTABLE_DATA);
    cpl_array *continuum
      = muse_cplarray_interpolate_table_linear(lambda, aSpectrum,
                                               "lambda", "flux");

    if (debug) {
      cpl_msg_debug(__func__, "Subtracting continuum from %"CPL_SIZE_FORMAT
                    " pixels", cpl_array_get_size(continuum));
    }
    cpl_array_subtract(data, continuum);
    cpl_array_delete(continuum);
    cpl_array_unwrap(data);
    cpl_array_unwrap(lambda);
    if (cpl_table_has_column(slice_pt->table, "lambda_double")) {
      cpl_table_erase_column(slice_pt->table, "lambda_double");
    }
  }
  muse_pixtable_extracted_delete(slice_pixtable);
  return CPL_ERROR_NONE;
}

/**@}*/
