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

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

#include "muse_sky.h"

#include "muse_cplwrappers.h"
#include "muse_data_format_z.h"
#include "muse_quality.h"
#include "muse_mask.h"
#include "muse_pfits.h"

/*----------------------------------------------------------------------------*
 *                          Special variable types                            *
 *----------------------------------------------------------------------------*/
/** @addtogroup muse_skysub */
/**@{*/

/*----------------------------------------------------------------------------*/
/**
    @brief Definition of the flux spectrum table structure.

    - <tt>lambda</tt>: wavelength [Angstrom]
    - <tt>flux</tt>: Flux [erg/(s cm^2 arcsec^2)]

    The table can be expected to be sorted by the wavelength.
 */
/*----------------------------------------------------------------------------*/
const muse_cpltable_def muse_fluxspectrum_def[] = {
  {"lambda", CPL_TYPE_DOUBLE, "Angstrom", "%7.2f", "wavelength", CPL_TRUE},
  {"flux", CPL_TYPE_DOUBLE, "erg/(s cm^2 arcsec^2)", "%e", "Flux", CPL_TRUE},
  { NULL, 0, NULL, NULL, NULL, CPL_FALSE }
};

/*----------------------------------------------------------------------------*/
/**
   @brief  Create a spectrum out of a cube by applying a mask.
   @param aCube MUSE datacube.
   @param aMask Mask image of sky regions.
   @return Table with @ref muse_dataspectrum_def "data spectrum".
*/
/*----------------------------------------------------------------------------*/
cpl_table *
muse_sky_spectrum_from_cube(muse_datacube *aCube, const cpl_mask *aMask) {
  unsigned count = cpl_imagelist_get_size(aCube->data);
  cpl_table *spectrum = muse_cpltable_new(muse_dataspectrum_def, count);
  double crval3 = cpl_propertylist_get_float(aCube->header, "CRVAL3");
  double crpix3 = cpl_propertylist_get_float(aCube->header, "CRPIX3");
  double cdelt3 = cpl_propertylist_get_float(aCube->header, "CD3_3");
  unsigned i;
  cpl_mask *nmask = cpl_mask_duplicate(aMask);
  cpl_mask_not(nmask);
  for (i = 0; i < count; i++) {
    cpl_table_set(spectrum, "lambda", i, crval3 + (i + 1 - crpix3) * cdelt3);

    cpl_image *img = cpl_imagelist_get(aCube->data, i);
    cpl_mask *bpm = cpl_image_get_bpm(img);
    cpl_mask_or(bpm, nmask);
    if (aCube->dq != NULL) {
      cpl_image *dq = cpl_imagelist_get(aCube->dq, i);
      cpl_mask *dq_mask = cpl_mask_threshold_image_create(dq, -0.5, 0.5);
      cpl_mask_not(dq_mask);
      cpl_mask_or(bpm, dq_mask);
      cpl_mask_delete(dq_mask);
    }
    double dev = cpl_image_get_stdev(img);
    double mean = cpl_image_get_mean(img);
    cpl_table_set(spectrum, "data", i, mean);
    cpl_table_set(spectrum, "stat", i, dev / sqrt(cpl_mask_count(bpm)));
    cpl_table_set(spectrum, "dq", i, (cpl_mask_count(bpm) < 3));
  }
  cpl_mask_delete(nmask);

  return spectrum;
}

/*----------------------------------------------------------------------------*/
/**
  @brief  Select spaxels to be considered as sky.
  @param  aImage      MUSE (white-light) image of the field of view.
  @param  aFraction   Fraction of spectra to select
  @param  aQCPrefix   prefix for the QC keywords
  @return A newly created bitmask for sky spaxels.

  Use thresholding to create a mask of sky regions.
  The output mask contains a QC parameter aQCPrefix" THRESHOLD" in its header
  component.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL, aImage is NULL}
 */
/*----------------------------------------------------------------------------*/
muse_mask *
muse_sky_create_skymask(muse_image *aImage, double aFraction,
                        const char *aQCPrefix)
{
  cpl_ensure(aImage, CPL_ERROR_NULL_INPUT, NULL);

  /* do statistics */
  muse_image_reject_from_dq(aImage);
  double t0 = cpl_image_get_min(aImage->data);
  double t1 = muse_cplimage_get_percentile(aImage->data, aFraction);
  cpl_msg_info(__func__, "Creating sky mask for pixels between minimum (%g) and"
               " threshold (%g)", t0, t1);

  /* create output mask with pixels between min and threshold */
  muse_mask *selectedRegions = muse_mask_new();
  selectedRegions->mask = cpl_mask_threshold_image_create(aImage->data, t0, t1);
  cpl_mask_not(selectedRegions->mask);
  cpl_mask_or(selectedRegions->mask, cpl_image_get_bpm(aImage->data));
  cpl_mask_not(selectedRegions->mask);

  /* add threshold as QC parameter */
  selectedRegions->header = cpl_propertylist_duplicate(aImage->header);
  char keyword[KEYWORD_LENGTH];
  snprintf(keyword, KEYWORD_LENGTH, "%s THRESHOLD", aQCPrefix);
  cpl_propertylist_append_double(selectedRegions->header, keyword, t1);

  return selectedRegions;
}

/*----------------------------------------------------------------------------*/
/**
    @brief Apply the LSF parameters to a spectrum.
    @param aLambda Wavelength bins [Angstrom]
    @param aLines @ref muse_sky_lines_lines_def "List of emission lines".
    @param aLsf LSF parameters
    @return the resulting spectrum, in count.
    @cpl_ensure{aLambda != NULL, CPL_ERROR_NULL_INPUT, NULL}
    @cpl_ensure{aLines != NULL, CPL_ERROR_NULL_INPUT, NULL}
    @cpl_ensure{aLsf != NULL, CPL_ERROR_NULL_INPUT, NULL}

    This function applies the instrument LSF to
    create a spectrum from a sky parametrization.

   @pseudocode
linespectrum = muse_sky_spectrum_set_lines(lines, aLsf)
spectrum = linespectrum + spectrum.continuum
return spectrum@endpseudocode
 */
/*----------------------------------------------------------------------------*/
cpl_array *
muse_sky_apply_lsf(const cpl_array *aLambda, const cpl_table *aLines,
                   const muse_lsf_params *aLsf)
{
  cpl_ensure(aLambda, CPL_ERROR_NULL_INPUT, NULL);
  cpl_ensure(aLsf, CPL_ERROR_NULL_INPUT, NULL);

  cpl_array *linesLambda = muse_cpltable_extract_column((cpl_table *)aLines,
                                                        "lambda");
  cpl_array *linesFlux = muse_cpltable_extract_column((cpl_table *)aLines,
                                                      "flux");
  cpl_array *spectrum
    = muse_lsf_spectrum_get_lines(aLambda, linesLambda, linesFlux, aLsf);
  cpl_array_unwrap(linesLambda);
  cpl_array_unwrap(linesFlux);

  cpl_array *sensitivity = cpl_array_duplicate(aLambda);
  cpl_array_subtract_scalar(sensitivity, aLsf->lambda_ref);
  muse_cplarray_poly1d(sensitivity, aLsf->sensitivity);

  cpl_array_multiply(spectrum, sensitivity);

  cpl_array_delete(sensitivity);

  return spectrum;
}

/*----------------------------------------------------------------------------*/
/**
  @brief    Mark all pixel above a certain limit as COSMIC
  @param    aSpectrum   Reference spectrum
  @param    aPixtable   Pixel table

  Check each selected pixel of the pixel table against the
  reference spectrum. If the data value of the pixel is higher than the
  according value of the spectrum + 5 * variance, it is considered as
  cosmic. This works well for sky since we assume a homogenious illumination,
  strong variations are not expected here.
 */
/*----------------------------------------------------------------------------*/
void
muse_sky_mark_cosmic(cpl_table *aSpectrum, muse_pixtable *aPixtable) {
  float *lbda = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_LAMBDA);
  float *data = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_DATA);
  int *dq = cpl_table_get_data_int(aPixtable->table, MUSE_PIXTABLE_DQ);

  cpl_size spec_nrows = cpl_table_get_nrow(aSpectrum);
  double *spec_data = cpl_table_get_data_double(aSpectrum, "data");
  double *spec_stat = cpl_table_get_data_double(aSpectrum, "stat");
  double *spec_std = cpl_malloc(spec_nrows * sizeof(double));
  cpl_size i_spec;
  for (i_spec = 0; i_spec < spec_nrows; i_spec++) {
    spec_std[i_spec] = sqrt(spec_stat[i_spec]);
  }

  /* loop through all selected pixel table rows  */
  cpl_array *asel = cpl_table_where_selected(aPixtable->table);
  const cpl_size *sel = cpl_array_get_data_cplsize_const(asel);
  cpl_size isel, nsel = cpl_array_get_size(asel);
  cpl_size n_cosmic = 0;
  for (isel = 0; isel < nsel; isel++) {
    cpl_size i_row = sel[isel];
    if ((dq[i_row] != EURO3D_GOODPIXEL)) {
      continue;
    }
    double l = lbda[i_row];
    i_spec = muse_cpltable_find_sorted(aSpectrum, "lambda", l);
    if ((i_spec < spec_nrows - 1) && (spec_data[i_spec] < spec_data[i_spec+1])) {
      i_spec++;
    }
    if (spec_data[i_spec] + 5 * spec_std[i_spec] < data[i_row]) {
      dq[i_row] = EURO3D_COSMICRAY;
      n_cosmic++;
    }
  }
  cpl_msg_info(__func__, "Marked %"CPL_SIZE_FORMAT" pixels of %"CPL_SIZE_FORMAT
               " as cosmic", n_cosmic, nsel);

  cpl_array_delete(asel);
  cpl_free(spec_std);
}

/*----------------------------------------------------------------------------*/
/**
  @brief  Save sky continuum table to file.
  @param  aProcessing   the processing structure
  @param  aContinuum    the sky continuum table
  @param  aHeader       the FITS header to use for the primary HDU
  @return CPL_ERROR_NONE on success, another CPL error code on failure

  The table extension is marked with and EXTNAME of "CONTINUUM".

  @error{return CPL_ERROR_NULL_INPUT, one of the arguments is NULL}
  @error{return CPL_ERROR_ILLEGAL_INPUT, the output frame could not be created}
 */
/*----------------------------------------------------------------------------*/
cpl_error_code
muse_sky_save_continuum(muse_processing *aProcessing,
                        const cpl_table *aContinuum, cpl_propertylist *aHeader)
{
  cpl_ensure_code(aProcessing && aContinuum && aHeader, CPL_ERROR_NULL_INPUT);
  cpl_frame *frame = muse_processing_new_frame(aProcessing, -1, aHeader,
                                               MUSE_TAG_SKY_CONT,
                                               CPL_FRAME_TYPE_TABLE);
  cpl_ensure_code(frame, CPL_ERROR_ILLEGAL_INPUT);
  const char *filename = cpl_frame_get_filename(frame);
  cpl_error_code rc = cpl_propertylist_save(aHeader, filename, CPL_IO_CREATE);
  rc = muse_cpltable_append_file(aContinuum, filename,
                                 "CONTINUUM", muse_fluxspectrum_def);
  if (rc == CPL_ERROR_NONE) {
    cpl_frameset_insert(aProcessing->outframes, frame);
  } else {
    cpl_frame_delete(frame);
  }
  return rc;
} /* muse_sky_save_continuum() */

/**@}*/
