/* -*- 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-2015 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_geometry_z.h"

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

/*---------------------------------------------------------------------------*/
/**
  @brief    Add the QC parameters to the spots measurements table.
  @param    aSpots    the output spots measurements table
  @param    aHeader   the header to modify

  Select all bright (flux > 1000 count) spots with valid FWHM values in each
  image of the series and add simple FWHM statistics for them. Also add mean
  and standard deviation of all average FWHM values across all exposures.
 */
/*---------------------------------------------------------------------------*/
static void
muse_geometry_qc_spots(cpl_table *aSpots, cpl_propertylist *aHeader)
{
  if (!aSpots || !aHeader) {
    return;
  }

  unsigned int nexp,
               nexp1 = cpl_table_get_column_min(aSpots, "image"),
               nexp2 = cpl_table_get_column_max(aSpots, "image");
  cpl_msg_debug(__func__, "Adding QC keywords for %u exposures to %s",
                nexp2 - nexp1 + 1, MUSE_TAG_SPOTS_TABLE);

  cpl_array *amean = cpl_array_new(nexp2 - nexp1 + 1, CPL_TYPE_DOUBLE);
  for (nexp = nexp1; nexp <= nexp2; nexp++) {
    cpl_table_unselect_all(aSpots);
    cpl_table_or_selected_int(aSpots, "image", CPL_EQUAL_TO, nexp);
    cpl_table_and_selected_double(aSpots, "xfwhm", CPL_GREATER_THAN, 0.);
    cpl_table_and_selected_double(aSpots, "yfwhm", CPL_GREATER_THAN, 0.);
    cpl_table_and_selected_double(aSpots, "flux", CPL_GREATER_THAN, 1000.);
    cpl_table *table = cpl_table_extract_selected(aSpots);
    /* compute the average of the x and y FWHM values */
    cpl_table_duplicate_column(table, "fwhm", table, "xfwhm");
    cpl_table_add_columns(table, "fwhm", "yfwhm");
    cpl_table_multiply_scalar(table, "fwhm", 0.5);
    cpl_errorstate es = cpl_errorstate_get();
    double mean = cpl_table_get_column_mean(table, "fwhm"),
           median = cpl_table_get_column_median(table, "fwhm"),
           stdev = cpl_table_get_column_stdev(table, "fwhm");
    cpl_table_delete(table);
    if (!cpl_errorstate_is_equal(es)) {
      cpl_errorstate_set(es);
      cpl_msg_debug(__func__, "QC parameters for spots in exposure %u likely "
                    "faulty", nexp);
    } else {
      /* only set mean value, if it's valid */
      cpl_array_set_double(amean, nexp - nexp1, mean);
    }

    char keyword[KEYWORD_LENGTH];
    snprintf(keyword, KEYWORD_LENGTH, QC_GEO_EXPk_MEAN, nexp);
    cpl_propertylist_update_float(aHeader, keyword, mean);
    snprintf(keyword, KEYWORD_LENGTH, QC_GEO_EXPk_MEDIAN, nexp);
    cpl_propertylist_update_float(aHeader, keyword, median);
    snprintf(keyword, KEYWORD_LENGTH, QC_GEO_EXPk_STDEV, nexp);
    cpl_propertylist_update_float(aHeader, keyword, stdev);
  } /* for nexp */
  cpl_propertylist_update_float(aHeader, QC_GEO_FWHM_MEAN,
                                cpl_array_get_mean(amean));
  cpl_propertylist_update_float(aHeader, QC_GEO_FWHM_STDEV,
                                cpl_array_get_stdev(amean));
  cpl_array_delete(amean);
} /* muse_geometry_qc_spots() */

/*---------------------------------------------------------------------------*/
/**
  @brief    Add the per-IFU QC parameters to the geometry table.
  @param    aGeoInit   the output geometry table
  @param    aHeader    the header to modify
  @param    aLambdas   vector with the wavelengths used

  The angle computed from the table at this stage is inverted by default, unless
  the environment variable MUSE_GEOMETRY_NO_INVERSION is set to a positive
  integer.
 */
/*---------------------------------------------------------------------------*/
static void
muse_geometry_qc_ifu(muse_geo_table *aGeoInit, cpl_propertylist *aHeader,
                     const cpl_vector *aLambdas)
{
  if (!aGeoInit || !aHeader || !aLambdas) {
    return;
  }
  cpl_table *gtinit = aGeoInit->table;
  const unsigned char ifu = cpl_table_get_int(gtinit, MUSE_GEOTABLE_FIELD, 0, NULL);
  /* normally flip the angles here */
  double angle = -cpl_table_get_column_mean(gtinit, MUSE_GEOTABLE_ANGLE),
         astdev = cpl_table_get_column_stdev(gtinit, MUSE_GEOTABLE_ANGLE),
         amedian = -cpl_table_get_column_median(gtinit, MUSE_GEOTABLE_ANGLE);
  /* if the geometry table later is /not/ flipped, flip mean/median back */
  if (getenv("MUSE_GEOMETRY_NO_INVERSION")
      && atoi(getenv("MUSE_GEOMETRY_NO_INVERSION")) > 0) {
    angle *= -1.;
    amedian *= -1.;
  }
  cpl_msg_debug(__func__, "Adding QC keywords for IFU %hhu: angle = %.3f +/- %.3f "
                "(%.3f) deg", ifu, angle, astdev, amedian);
  char *keyword = cpl_sprintf(QC_GEO_IFUi_ANGLE, ifu);
  cpl_propertylist_update_float(aHeader, keyword, amedian);
  cpl_free(keyword);

  int i, n = cpl_vector_get_size(aLambdas);
  for (i = 1; i <= n; i++) {
    double lambda = cpl_vector_get(aLambdas, i - 1);
    char *kw = cpl_sprintf(QC_GEO_IFUi_WLENj, ifu, i),
         *kwmean = cpl_sprintf(QC_GEO_IFUi_MEANj, ifu, i),
         *kwmedi = cpl_sprintf(QC_GEO_IFUi_MEDIANj, ifu, i),
         *kwstdv = cpl_sprintf(QC_GEO_IFUi_STDEVj, ifu, i);
    cpl_propertylist_update_float(aHeader, kw, lambda);

    cpl_table_unselect_all(gtinit);
    cpl_table_or_selected_double(gtinit, "lambda", CPL_EQUAL_TO, lambda);
    if (cpl_table_count_selected(gtinit) < 1) { /* fill in dummy values */
      cpl_propertylist_update_float(aHeader, kwmean, i);
      cpl_propertylist_update_float(aHeader, kwmedi, i);
      cpl_propertylist_update_float(aHeader, kwstdv, i);
    } else {
      cpl_table *t = cpl_table_extract_selected(gtinit);
      cpl_propertylist_update_float(aHeader, kwmean,
                                    cpl_table_get_column_mean(t, "flux"));
      cpl_propertylist_update_float(aHeader, kwmedi,
                                    cpl_table_get_column_median(t, "flux"));
      cpl_propertylist_update_float(aHeader, kwstdv,
                                    cpl_table_get_column_stdev(t, "flux"));
      cpl_table_delete(t);
    }
    cpl_free(kw);
    cpl_free(kwmean);
    cpl_free(kwmedi);
    cpl_free(kwstdv);
  } /* for i (all lambdas) */
} /* muse_geometry_qc_ifu() */

/*---------------------------------------------------------------------------*/
/**
  @brief    Load and pre-process the raw images (or skip loading them).
  @param    aProcessing  the processing structure
  @param    aIFU         the IFU number
  @return   The imagelist of pre-reduced exposures.
 */
/*---------------------------------------------------------------------------*/
static muse_imagelist *
muse_geometry_load_images(muse_processing *aProcessing, unsigned short aIFU)
{
  unsigned int skip = 0;
  if (getenv("MUSE_GEOMETRY_SKIP") && atoi(getenv("MUSE_GEOMETRY_SKIP")) > 0) {
    skip = atoi(getenv("MUSE_GEOMETRY_SKIP"));
  }

  muse_imagelist *images;
  if (!skip) { /* going all the way from raw exposures */
    /* Find MASTER_BIAS file and load the relevant FITS headers, to see *
     * what basic processing parameters we need to take over from it.   */
    cpl_frame *fbias = muse_frameset_find_master(aProcessing->inframes,
                                                 MUSE_TAG_MASTER_BIAS, aIFU);
    /* Merged or not, the REC1 headers we need here are in the primary HDU: */
    cpl_propertylist *hbias = cpl_propertylist_load(cpl_frame_get_filename(fbias), 0);
    cpl_frame_delete(fbias);
    muse_basicproc_params *bpars = muse_basicproc_params_new_from_propertylist(hbias);
    cpl_propertylist_delete(hbias);
    images = muse_basicproc_load(aProcessing, aIFU, bpars);
    muse_basicproc_params_delete(bpars);
  } else { /* skipping some part of the loading/processing */
    images = muse_imagelist_new();
    /* filenames are "MASK_REDUCED_00ii-jj.fits" */
    unsigned int ifile, nfiles = 99; /* first assume that we will read many files */
    if (skip >= 2) {
      cpl_msg_warning(__func__, "Skipping spot measurements, only loading first"
                      " (reduced) file for IFU %02hu", aIFU);
      nfiles = 1;
    } else {
      cpl_msg_warning(__func__, "Skipping raw image processing, loading all "
                      "reduced images directly");
    }
    for (ifile = 0; ifile < nfiles; ifile++) {
      char *fn = cpl_sprintf("MASK_REDUCED_00%02u-%02hu.fits", ifile + 1, aIFU);
      cpl_errorstate es = cpl_errorstate_get();
      muse_image *image = muse_image_load(fn);
      if (!image) {
        cpl_errorstate_set(es); /* ignore the errors */
        cpl_free(fn);
        break;
      }
      cpl_frame *frame = cpl_frame_new();
      cpl_frame_set_filename(frame, fn);
      cpl_frame_set_tag(frame, "MASK");
      muse_processing_append_used(aProcessing, frame, CPL_FRAME_GROUP_RAW, 0);
      cpl_msg_debug(__func__, "file = %s", fn);
      muse_imagelist_set(images, image, ifile);
      cpl_free(fn);
    } /* for ifile */
    cpl_msg_debug(__func__, "Read %u file%s", ifile, ifile == 1 ? "" : "s");
  } /* else (skipped some loading/processing) */
  return images;
} /* muse_geometry_load_images() */

/*----------------------------------------------------------------------------*/
/**
  @brief    Reconstruct cube plus images from the combined input MASK exposures.
  @param    aProcessing  the processing structure
  @param    aParams      the parameters list
  @param    aGeo         the MUSE geometry table to verify
  @param    aLMin        minimum wavelength cut before reconstruction
  @param    aLMax        maximum wavelength cut before reconstruction
  @return   CPL_ERROR_NONE on success or another cpl_error_code on failure

  Use all MUSE_COMBINED files that were previously created, re-load them from
  disk, create and merge pixel tables for them using the geometry table aGeo,
  restrict the wavelength to between aLMin and aLMax, and reconstruct the cube.

  Afterwards create a white-light image and save both as cube.

  @error{set and return CPL_ERROR_NULL_INPUT,
         aProcessing\, aParams\, aGeo\, or aGeo->table are NULL}
  @error{set and return CPL_ERROR_ILLEGAL_INPUT,
         aLMin is larger or equal to aLMax}
  @error{set CPL_ERROR_ILLEGAL_OUTPUT and appropriate message but continue,
         after cutting in wavelength, no pixels are left}
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
muse_geometry_reconstruct_combined(muse_processing *aProcessing,
                                   muse_geometry_params_t *aParams,
                                   muse_geo_table *aGeo,
                                   double aLMin, double aLMax)
{
  cpl_ensure_code(aProcessing && aParams && aGeo && aGeo->table,
                  CPL_ERROR_NULL_INPUT);
  if (aLMin >= aLMax) {
    cpl_msg_warning(__func__, "Invalid wavelength range for reconstruction "
                    "(%.2f..%.2f), skipping combined reconstruction!", aLMin, aLMax);
    return CPL_ERROR_ILLEGAL_INPUT;
  }
  cpl_msg_info(__func__, "Reconstructing combined input as cube in the "
               "wavelength range %.2f..%.2f.", aLMin, aLMax);
  unsigned int skip = 0;
  if (getenv("MUSE_GEOMETRY_SKIP") && atoi(getenv("MUSE_GEOMETRY_SKIP")) > 0) {
    skip = atoi(getenv("MUSE_GEOMETRY_SKIP"));
  }
  cpl_table *gt = aGeo->table;

  muse_pixtable **pts = cpl_calloc(kMuseNumIFUs, sizeof(muse_pixtable));
  unsigned char nifu;

  // FIXME: Removed default(none) clause here, to make the code work with
  //        gcc9 while keeping older versions of gcc working too without
  //        resorting to conditional compilation. Having default(none) here
  //        causes gcc9 to abort the build because of __func__ not being
  //        specified in a data sharing clause. Adding a data sharing clause
  //        makes older gcc versions choke.
  #pragma omp parallel for                                                     \
          shared(gt, aParams, aProcessing, pts, skip)
  for (nifu = (unsigned)aParams->ifu1; nifu <= (unsigned)aParams->ifu2; nifu++) {
    cpl_table *trace = muse_processing_load_ctable(aProcessing, MUSE_TAG_TRACE_TABLE, nifu),
              *wavecal = muse_processing_load_ctable(aProcessing, MUSE_TAG_WAVECAL_TABLE, nifu);
    if (!trace || !wavecal) {
      cpl_table_delete(trace);
      cpl_table_delete(wavecal);
      continue; /* just skip this IFU */
    }
    /* now load again the file we saved previously */
    cpl_frame *cframe = NULL;
    if (skip == 2) { /* we are running with MUSE_GEOMETRY_SKIP=2 */
      /* construct frame manually, assuming a local file */
      cframe = cpl_frame_new();
      char *fn = cpl_sprintf("MASK_COMBINED-%02hhu.fits", nifu);
      cpl_frame_set_filename(cframe, fn);
      cpl_free(fn);
    } else {
      cframe = muse_frameset_find_master(aProcessing->outframes,
                                         MUSE_TAG_MASK_COMBINED, nifu);
    }
    if (!cframe) {
      cpl_table_delete(trace);
      cpl_table_delete(wavecal);
      continue;
    }
    cpl_msg_debug(__func__, "reconstructing IFU %2hhu using \"%s\"", nifu,
                  cpl_frame_get_filename(cframe));
    muse_image *combined = muse_image_load(cpl_frame_get_filename(cframe));
    cpl_frame_delete(cframe);
    pts[nifu - 1] = muse_pixtable_create(combined, trace, wavecal, gt);
    cpl_table_delete(trace);
    cpl_table_delete(wavecal);
    muse_image_delete(combined);
    if (!pts[nifu - 1]) {
      cpl_msg_warning(__func__, "Could not create a pixel table for reconstruction"
                      " for IFU %2hhu!", nifu++);
    }
  } /* for nifu (all IFUs) */

  /* merge all pixel tables                                               *
   * a la muse_pixtable_load_merge_channels() but without flux correction */
  nifu = aParams->ifu1;
  muse_pixtable *pt = pts[nifu - 1];
  pts[nifu - 1] = NULL; /* to not free it too early */
  while (!pt && nifu <= aParams->ifu2) { /* in case we picked a non-existing table */
    nifu++;
    pt = pts[nifu - 1];
    pts[nifu - 1] = NULL; /* to not free it too early */
  } /* while */
  if (pt) {
    for ( ; nifu <= (unsigned)aParams->ifu2; nifu++) {
      if (pts[nifu - 1]) {
        cpl_table_insert(pt->table, pts[nifu - 1]->table, muse_pixtable_get_nrow(pt));
        muse_pixtable_delete(pts[nifu - 1]);
      } /* if */
    } /* for nifu (all IFUs) */
  } /* if pt */
  cpl_free(pts);

  /* cut the cube, for faster reconstruction speed */
  muse_pixtable_restrict_wavelength(pt, aLMin, aLMax);
  if (muse_pixtable_get_nrow(pt) < 1) { /* will also cause return for NULL pt */
    muse_pixtable_delete(pt);
    return cpl_error_set_message(__func__, CPL_ERROR_ILLEGAL_OUTPUT,
                                 "After cutting to %.2f..%.2f in wavelength no "
                                 "pixels for image reconstruction are left!",
                                 aLMin, aLMax);
  }

  /* now just reconstruct the cube and collapse over the full range */
  muse_resampling_params *rp = muse_resampling_params_new(MUSE_RESAMPLE_WEIGHTED_DRIZZLE);
  rp->crsigma = -1.; /* no cr cleaning */
  muse_datacube *cube = muse_resampling_cube(pt, rp, NULL);
  muse_resampling_params_delete(rp);
  muse_pixtable_delete(pt);
  /* also create a corresponding white-light imgae */
  muse_image *white = muse_datacube_collapse(cube, NULL);
  cube->recimages = muse_imagelist_new();
  cube->recnames = cpl_array_new(1, CPL_TYPE_STRING);
  muse_imagelist_set(cube->recimages, white, 0);
  cpl_array_set_string(cube->recnames, 0, "white");
  muse_processing_save_cube(aProcessing, -1, cube, "GEOMETRY_CUBE",
                            MUSE_CUBE_TYPE_FITS);
  muse_datacube_delete(cube);

  return CPL_ERROR_NONE;
} /* muse_geometry_reconstruct_combined() */

/*----------------------------------------------------------------------------*/
/**
  @brief    Reconstruct images from MASK_CHECK exposures in the input frameset.
  @param    aProcessing  the processing structure
  @param    aParams      the parameters list
  @param    aGeo         the MUSE geometry table to verify
  @param    aLMin        minimum wavelength cut before reconstruction
  @param    aLMax        maximum wavelength cut before reconstruction
  @return   CPL_ERROR_NONE on success or another cpl_error_code on failure

  For all MASK_CHECK exposures found, create pixel tables are some very basic
  processing (only bias subtraction and trimming), concatenate all pixel tables
  and use them to reconstruct a cube with only one (relevant) plane. Save this
  image plane, disregarding the variance extension.

  All this is to have some image to save that can be used to visually check the
  created geometry table. Ideally, the MASK_CHECK input exposures are from the
  trace mask at several angles.

  @error{set and return CPL_ERROR_NULL_INPUT,
         aProcessing\, aParams\, aGeo\, or aGeo->table are NULL}
  @error{set and return CPL_ERROR_ILLEGAL_INPUT,
         aLMin is larger or equal to aLMax}
  @error{set CPL_ERROR_ILLEGAL_OUTPUT and appropriate message but continue,
         after cutting in wavelength, no pixels are left}
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
muse_geometry_reconstruct(muse_processing *aProcessing,
                          muse_geometry_params_t *aParams,
                          muse_geo_table *aGeo, double aLMin, double aLMax)
{
  cpl_ensure_code(aProcessing && aParams && aGeo && aGeo->table,
                  CPL_ERROR_NULL_INPUT);
  if (aLMin >= aLMax) {
    cpl_msg_warning(__func__, "Invalid wavelength range for reconstruction "
                    "(%.2f..%.2f), skipping reconstruction!", aLMin, aLMax);
    return CPL_ERROR_ILLEGAL_INPUT;
  }

  /* convert all existing raw frames in the used frames to             *
   * CPL_FRAME_GROUP_CALIB so that the dependency of the GEOMETRY_CHECK *
   * output is clear, but keep the original usedframes around          */
  cpl_frameset *usedoriginal = cpl_frameset_duplicate(aProcessing->usedframes);
  int iframe, nframes = cpl_frameset_get_size(aProcessing->usedframes);
  for (iframe = 0; iframe < nframes; iframe++) {
    cpl_frame *frame = cpl_frameset_get_position(aProcessing->usedframes, iframe);
    if (cpl_frame_get_group(frame) == CPL_FRAME_GROUP_RAW) {
      cpl_frame_set_group(frame, CPL_FRAME_GROUP_CALIB);
    } /* if */
  } /* for iframe */

  cpl_table *gt = aGeo->table;
  cpl_frameset *frames = muse_frameset_find(aProcessing->inframes,
                                            "MASK_CHECK", 0, CPL_FALSE);
  nframes = cpl_frameset_get_size(frames);
  cpl_msg_info(__func__, "Found %d datasets to reconstruct", nframes);
  for (iframe = 0; iframe < nframes; iframe++) {
    cpl_frame *frame = cpl_frameset_get_position(frames, iframe);
    const char *fn = cpl_frame_get_filename(frame);
    cpl_msg_info(__func__, "Reconstructing image %d (from \"%s\"), using "
                 "wavelength range %.2f..%.2f", iframe + 1, fn, aLMin, aLMax);
    muse_pixtable **pts = cpl_calloc(kMuseNumIFUs, sizeof(muse_pixtable));
    unsigned char nifu;

    // FIXME: Removed default(none) clause here, to make the code work with
    //        gcc9 while keeping older versions of gcc working too without
    //        resorting to conditional compilation. Having default(none) here
    //        causes gcc9 to abort the build because of __func__ not being
    //        specified in a data sharing clause. Adding a data sharing clause
    //        makes older gcc versions choke.
    #pragma omp parallel for                                                   \
            shared(aParams, aProcessing, fn, frames, gt, pts)
    for (nifu = (unsigned)aParams->ifu1; nifu <= (unsigned)aParams->ifu2; nifu++) {
      cpl_table *trace = muse_processing_load_ctable(aProcessing, MUSE_TAG_TRACE_TABLE, nifu),
                *wavecal = muse_processing_load_ctable(aProcessing, MUSE_TAG_WAVECAL_TABLE, nifu);
      if (!trace || !wavecal) {
        cpl_table_delete(trace);
        cpl_table_delete(wavecal);
        continue; /* just skip this IFU */
      }
      cpl_frame *bframe = muse_frameset_find_master(aProcessing->inframes,
                                                    MUSE_TAG_MASTER_BIAS, nifu);
      if (!bframe) {
        cpl_table_delete(trace);
        cpl_table_delete(wavecal);
        continue;
      }
      const char *bname = cpl_frame_get_filename(bframe);
      cpl_errorstate es = cpl_errorstate_get();
      muse_image *bias = muse_image_load(bname);
      if (!bias) {
        cpl_errorstate_set(es); /* ignore error for case of merged file */
        bias = muse_image_load_from_extensions(bname, nifu);
      }
      cpl_frame_delete(bframe);
      /* don't want all the overhead of muse_basicproc_load(), *
       * do part of its processing manually                    */
      int ext = muse_utils_get_extension_for_ifu(fn, nifu);
      muse_image *raw = muse_image_load_from_raw(fn, ext);
      if (!raw) {
        cpl_table_delete(trace);
        cpl_table_delete(wavecal);
        muse_image_delete(bias);
        continue; /* just skip this IFU */
      }
      muse_basicproc_params *bpars = muse_basicproc_params_new_from_propertylist(bias->header);
      muse_quadrants_overscan_stats(raw, bpars ? bpars->rejection : "dcr", 0);
      if (bpars && !strncmp(bpars->overscan, "vpoly", 5)) {
        /* vertical polyfit requested, see if there are more parameters */
        unsigned char ovscvorder = 3;
        double frms = 1.01,
               fchisq = 1.04;
        char *rest = strchr(bpars->overscan, ':');
        if (strlen(bpars->overscan) > 6 && rest++) { /* try to access info after "vpoly:" */
          ovscvorder = strtol(rest, &rest, 10);
          if (strlen(rest++) > 0) { /* ++ to skip over the comma */
            frms = strtod(rest, &rest);
            if (strlen(rest++) > 0) {
              fchisq = strtod(rest, &rest);
            }
          }
        } /* if */
        muse_quadrants_overscan_polyfit_vertical(raw, bpars->ovscignore,
                                                 ovscvorder, bpars->ovscsigma,
                                                 frms, fchisq);
      } /* if bpars and vpoly */
      muse_image *image = muse_quadrants_trim_image(raw);
      muse_image_delete(raw);
      muse_image_variance_create(image, bias);
      if (bpars && !strncmp(bpars->overscan, "offset", 6)) {
        muse_quadrants_overscan_correct(image, bias);
      } /* if bpars and offset */
      muse_basicproc_params_delete(bpars);
      muse_image_subtract(image, bias);
      muse_image_delete(bias);
      muse_image_adu_to_count(image);
      pts[nifu - 1] = muse_pixtable_create(image, trace, wavecal, gt);
      cpl_table_delete(trace);
      cpl_table_delete(wavecal);
      muse_image_delete(image);
      if (!pts[nifu - 1]) {
        cpl_msg_warning(__func__, "Could not create a pixel table for "
                        "reconstruction for IFU %2hhu!", nifu++);
      }
    } /* for nifu (all IFUs) */

    /* merge all pixel tables                                               *
     * a la muse_pixtable_load_merge_channels() but without flux correction */
    nifu = aParams->ifu1;
    muse_pixtable *pt = pts[nifu - 1];
    pts[nifu - 1] = NULL; /* to not free it too early */
    while (!pt && nifu <= aParams->ifu2) { /* in case we picked a non-existing table */
      nifu++;
      pt = pts[nifu - 1];
      pts[nifu - 1] = NULL; /* to not free it too early */
    } /* while */
    if (pt) {
      for ( ; nifu <= (unsigned)aParams->ifu2; nifu++) {
        if (pts[nifu - 1]) {
          cpl_table_insert(pt->table, pts[nifu - 1]->table, muse_pixtable_get_nrow(pt));
          muse_pixtable_delete(pts[nifu - 1]);
        } /* if */
      } /* for nifu (all IFUs) */
    } /* if pt */
    cpl_free(pts);

    /* cut the cube, for faster reconstruction speed */
    muse_pixtable_restrict_wavelength(pt, aLMin, aLMax);
    if (muse_pixtable_get_nrow(pt) < 1) {
      cpl_error_set_message(__func__, CPL_ERROR_ILLEGAL_OUTPUT,
                            "After cutting to %.2f..%.2f in wavelength no "
                            "pixels for image reconstruction of \"%s\" are "
                            "left!", aLMin, aLMax, fn);
      muse_pixtable_delete(pt);
      continue;
    }

    /* now just reconstruct the cube and collapse over the full range */
    muse_resampling_params *rp = muse_resampling_params_new(MUSE_RESAMPLE_WEIGHTED_DRIZZLE);
    rp->crsigma = -1.; /* no cr cleaning */
    /* 10000 Angstrom large pixels -> just one relevant plane! *
     * (there will be a second but empty plane)                */
    rp->dlambda = 10000.;
    muse_datacube *cube = muse_resampling_cube(pt, rp, NULL);
    muse_resampling_params_delete(rp);
    muse_pixtable_delete(pt);
    /* I don't want all the cube stuff in the header, better derive it    *
     * from the original primary header of the raw MASK_CHECK exposure.   *
     * Use a trick so that the MASK_CHECK files appears first in the used *
     * frames and appear in the headers.                                  */
    muse_processing_append_used(aProcessing, frame, CPL_FRAME_GROUP_RAW, 1);
#pragma omp critical(muse_processing_used_frames)
    cpl_frameset_insert(usedoriginal, cpl_frame_duplicate(frame)); /* also add here */
    cpl_propertylist *header = cpl_propertylist_load(fn, 0);
    muse_processing_save_cimage(aProcessing, -1, cpl_imagelist_get(cube->data, 0),
                                header, "GEOMETRY_CHECK");
    cpl_propertylist_delete(header);
    muse_datacube_delete(cube);
  } /* for iframe: all found MASK_CHECK frames */
  cpl_frameset_delete(frames);
  cpl_frameset_delete(aProcessing->usedframes);
  aProcessing->usedframes = usedoriginal;

  return CPL_ERROR_NONE;
} /* muse_geometry_reconstruct() */

/*----------------------------------------------------------------------------*/
/**
  @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
 */
/*----------------------------------------------------------------------------*/
int
muse_geometry_compute(muse_processing *aProcessing,
                      muse_geometry_params_t *aParams)
{
  /* set up centroiding type */
  muse_geo_centroid_type centroid = MUSE_GEO_CENTROID_BARYCENTER;
  if (aParams->centroid == MUSE_GEOMETRY_PARAM_CENTROID_GAUSSIAN) {
    centroid = MUSE_GEO_CENTROID_GAUSSIAN;
  } else if (aParams->centroid != MUSE_GEOMETRY_PARAM_CENTROID_BARYCENTER) {
    cpl_msg_error(__func__, "unknown centroiding method \"%s\"", aParams->centroid_s);
    return -1;
  }

  /* load linelist upfront, to not having to do that in each thread */
  muse_table *linelist = muse_processing_load_table(aProcessing,
                                                    MUSE_TAG_LINE_CATALOG, 0);
  cpl_boolean listvalid = muse_wave_lines_check(linelist);
  cpl_vector *vlines = muse_geo_lines_get(linelist->table); /* suitable lines */
  muse_table_delete(linelist);
  if (!listvalid || !vlines) {
    cpl_msg_error(__func__, "%s could not be loaded/verified or enough suitable"
                  " lines are missing", MUSE_TAG_LINE_CATALOG);
    cpl_vector_delete(vlines);
    return -1;
  }

  cpl_msg_info(__func__, "Analyzing IFUs %d to %d", aParams->ifu1, aParams->ifu2);
  cpl_error_code rc[kMuseNumIFUs];
  cpl_table *mspots[kMuseNumIFUs],
            *trace[kMuseNumIFUs],
            *wavecal[kMuseNumIFUs];
  cpl_propertylist *headfinal = NULL;
  cpl_array *dy = cpl_array_new(0, CPL_TYPE_DOUBLE);
  unsigned int nifu;

  // FIXME: Removed default(none) clause here, to make the code work with
  //        gcc9 while keeping older versions of gcc working too without
  //        resorting to conditional compilation. Having default(none) here
  //        causes gcc9 to abort the build because of __func__ not being
  //        specified in a data sharing clause. Adding a data sharing clause
  //        makes older gcc versions choke.
  #pragma omp parallel for                                                     \
          shared(aParams, aProcessing, centroid, dy, headfinal, mspots, rc,    \
                 trace, vlines, wavecal)
  for (nifu = (unsigned)aParams->ifu1; nifu <= (unsigned)aParams->ifu2; nifu++) {
    rc[nifu - 1] = CPL_ERROR_NONE; /* signify success for this thread by default */
    mspots[nifu - 1] = NULL;

    /* load and check trace and wavecal tables early-on, *
     * to be able to quickly return on error             */
    trace[nifu - 1] = muse_processing_load_ctable(aProcessing, MUSE_TAG_TRACE_TABLE, nifu);
    wavecal[nifu - 1] = muse_processing_load_ctable(aProcessing, MUSE_TAG_WAVECAL_TABLE, nifu);
    if (!trace[nifu - 1] || !wavecal[nifu - 1]) {
      cpl_msg_error(__func__, "Calibration could not be loaded for IFU %u: %s%s",
                    nifu, !trace[nifu - 1] ? " "MUSE_TAG_TRACE_TABLE : "",
                    !wavecal[nifu - 1] ? " "MUSE_TAG_WAVECAL_TABLE : "");
      cpl_table_delete(trace[nifu - 1]);
      cpl_table_delete(wavecal[nifu - 1]);
      trace[nifu - 1] = NULL;
      wavecal[nifu - 1] = NULL;
      rc[nifu - 1] = CPL_ERROR_NULL_INPUT;
      continue;
    }

    /* load in separate function to allow for the lengthy *
     * "skip" option handling from the environment        */
    muse_imagelist *images = muse_geometry_load_images(aProcessing, nifu);
    if (!images) {
      cpl_msg_error(__func__, "Loading and basic processing of the raw input "
                    "images failed for IFU %u!", nifu);
      cpl_table_delete(trace[nifu - 1]);
      cpl_table_delete(wavecal[nifu - 1]);
      trace[nifu - 1] = NULL;
      wavecal[nifu - 1] = NULL;
      rc[nifu - 1] = cpl_error_get_code();
      continue;
    }

    unsigned int skip = 0;
    if (getenv("MUSE_GEOMETRY_SKIP") && atoi(getenv("MUSE_GEOMETRY_SKIP")) > 0) {
      skip = atoi(getenv("MUSE_GEOMETRY_SKIP"));
    }
    cpl_propertylist *header;
    if (skip >= 2) { /* directly skip to existing spots table */
      char *fn = cpl_sprintf("SPOTS_TABLE-%02u.fits", nifu);
      cpl_msg_warning(__func__, "Reading spot measurements from \"%s\"", fn);
      mspots[nifu - 1] = cpl_table_load(fn, 1, 1);
      cpl_free(fn);
    } else {
      muse_image *av = muse_combine_average_create(images);
      cpl_propertylist_append(av->header, muse_imagelist_get(images, 0)->header);
      muse_processing_save_image(aProcessing, nifu, av, MUSE_TAG_MASK_COMBINED);
      muse_image_reject_from_dq(av); /* make sure to recognize bad pixels */
      mspots[nifu - 1] = muse_geo_measure_spots(av, images, trace[nifu - 1],
                                                wavecal[nifu - 1], vlines,
                                                aParams->sigma, centroid);
      muse_image_delete(av);
      /* now save this intermediate result */
      header = cpl_propertylist_duplicate(muse_imagelist_get(images, 0)->header);
      /* does this QC really belong into the spots table, or should that *
       * just be for debugging and the QC go into the geometry table?    */
      muse_geometry_qc_spots(mspots[nifu - 1], header);
      muse_processing_save_table(aProcessing, nifu, mspots[nifu - 1], header,
                                 MUSE_TAG_SPOTS_TABLE, MUSE_TABLE_TYPE_CPL);
      cpl_propertylist_delete(header);
    } /* else: measure all spots */
    cpl_msg_info(__func__, "measured %"CPL_SIZE_FORMAT" spots in %u images",
                 cpl_table_get_nrow(mspots[nifu - 1]), muse_imagelist_get_size(images));
    cpl_table_delete(wavecal[nifu - 1]);

    if (!skip) { /* we started from the raw images */
      /* save reduced images after the measurement because while setting up     *
       * the output header we delete the MUSE_HDR_TMP_FN keyword from the image */
      unsigned int k;
      for (k = 0; k < muse_imagelist_get_size(images); k++) {
        muse_image *image = muse_imagelist_get(images, k);
        muse_processing_save_image(aProcessing, nifu, image,
                                   MUSE_TAG_MASK_REDUCED);
      } /* for k (all images) */
    } /* if !skip */
    #pragma omp critical (muse_geo_header_construct)
    {
    if (!headfinal) {
      headfinal = cpl_propertylist_duplicate(muse_imagelist_get(images, 0)->header);
      /* Remove some properties that we don't need in the output file. *
       * They refer to the specific extension and should not be        *
       * present in the output file that is global to the instrument.  *
       * Some may be put back from the first raw input file by         *
       * cpl_dfs_setup_product_header(), but at least try...           */
      cpl_propertylist_erase_regexp(headfinal,
                                    "EXTNAME|ESO DET (CHIP|OUT)|ESO DET2", 0);
    } /* if */
    }
    muse_imagelist_delete(images);
    /* compute the effective vertical pinhole distance per IFU */
    muse_geo_compute_pinhole_local_distance(dy, mspots[nifu - 1]);
  } /* for nifu (all IFUs) */
  /* now use the full array for the global statistics to get a final pinhole distance */
  double pinholedy = muse_geo_compute_pinhole_global_distance(dy, 0.001, 0.5, 0.8);
  /* return the final value, but don't do anything with it, since for  *
   * the moment it is already set in the environment (in the function) */
  UNUSED_ARGUMENT(pinholedy);
  cpl_array_delete(dy);

  /* variables for combined output tables */
  cpl_table *spots = NULL;
  muse_geo_table *geotable = NULL;

  // FIXME: Removed default(none) clause here, to make the code work with
  //        gcc9 while keeping older versions of gcc working too without
  //        resorting to conditional compilation. Having default(none) here
  //        causes gcc9 to abort the build because of __func__ not being
  //        specified in a data sharing clause. Adding a data sharing clause
  //        makes older gcc versions choke.
  #pragma omp parallel for                                                     \
          shared(aParams, geotable, headfinal, mspots, spots, trace, vlines)
  for (nifu = (unsigned)aParams->ifu1; nifu <= (unsigned)aParams->ifu2; nifu++) {
    /* finally compute the geometry, initial and horizontal parts for one IFU */
    muse_geo_table *geoinit = muse_geo_determine_initial(mspots[nifu - 1], trace[nifu - 1]);
    muse_geo_table *geohori = muse_geo_determine_horizontal(geoinit);
    cpl_table_delete(trace[nifu - 1]);
    /* add QC parameters and save the result, here, the initial geometry is needed */
    #pragma omp critical (muse_geo_header_construct)
    muse_geometry_qc_ifu(geoinit, headfinal, vlines);
    muse_geo_table_delete(geoinit);

    /* prepare the combined tables (geometry and spots) */
    #pragma omp critical (muse_geo_table_insert)
    {
    if (!geotable) {
      geotable = geohori;
    } else {
      if (fabs(geotable->scale - geohori->scale) > 1e-10) {
        /* for some reason, the difference it typically 2.5e-13, but we *
         * just want to ensure that both are about 1.7050 +/- 0.0001 or *
         * so and for that an accuracy of 1e-10 is more than enough     */
        cpl_msg_warning(__func__, "Combined and single geometry tables have "
                        "different scales (%.15f / %.15f)!", geotable->scale,
                        geohori->scale);
      }
      cpl_table_insert(geotable->table, geohori->table,
                       cpl_table_get_nrow(geotable->table));
      muse_geo_table_delete(geohori);
    }
    }
    #pragma omp critical (muse_spots_table_insert)
    {
    if (!spots) {
      spots = mspots[nifu - 1];
    } else {
      cpl_table_insert(spots, mspots[nifu - 1], cpl_table_get_nrow(spots));
      cpl_table_delete(mspots[nifu - 1]);
    }
    }
  } /* for nifu (all IFUs) */
  cpl_vector_delete(vlines);
  cpl_error_code result = CPL_ERROR_NONE;
  /* non-parallel loop to check for return codes */
  for (nifu = (unsigned)aParams->ifu1; nifu <= (unsigned)aParams->ifu2; nifu++) {
    if (result < rc[nifu - 1]) {
      result = rc[nifu - 1];
    } /* if */
  } /* for nifu (all IFUs) */
#if 0
  cpl_table_save(geotable, NULL, NULL, "geocombined.fits", CPL_IO_CREATE);
  cpl_table_save(spots, NULL, NULL, "spotscombined.fits", CPL_IO_CREATE);
#endif
  muse_geo_refine_horizontal(geotable, spots);
  cpl_table_delete(spots);

  muse_geo_table *geofinal = muse_geo_determine_vertical(geotable);
  muse_geo_table_delete(geotable);
  cpl_error_code rcfinal = muse_geo_finalize(geofinal);
  if (result < rcfinal) {
    result = rcfinal;
  }
  /* smooth the result per slicer stack, if sigma > 0. */
  if (aParams->smooth > 0.) {
    /* save this state already */
    if (geofinal) {
      muse_geo_qc_global(geofinal, headfinal);
      muse_processing_save_table(aProcessing, -1, geofinal->table, headfinal,
                                 MUSE_TAG_GEOMETRY_OLD, MUSE_TABLE_TYPE_CPL);
    }
    rcfinal = muse_geo_correct_slices(geofinal, headfinal, aParams->smooth);
    if (result < rcfinal) {
      result = rcfinal;
    }
  } else {
    /* add fake entries to signify that this wasn't smoothed */
    cpl_propertylist_update_int(headfinal, QC_GEO_SMOOTH_NX, -1);
    cpl_propertylist_update_int(headfinal, QC_GEO_SMOOTH_NY, -1);
    cpl_propertylist_update_int(headfinal, QC_GEO_SMOOTH_NA, -1);
    cpl_propertylist_update_int(headfinal, QC_GEO_SMOOTH_NW, -1);
  }
  /* now this is the really final table that we want to save */
  if (geofinal) {
    /* compute (or update) the global QC parameters */
    muse_geo_qc_global(geofinal, headfinal);
    muse_processing_save_table(aProcessing, -1, geofinal->table, headfinal,
                               MUSE_TAG_GEOMETRY_TABLE, MUSE_TABLE_TYPE_CPL);
  }
  /* first, try to reconstruct the (average-)combined images */
  muse_geometry_reconstruct_combined(aProcessing, aParams, geofinal,
                                     aParams->lambdamin, aParams->lambdamax);
  /* try to reconstruct some images using this final geometry table, if   *
   * no MASK_CHECK files were passed to this recipe, this will do nothing */
  muse_geometry_reconstruct(aProcessing, aParams, geofinal,
                            aParams->lambdamin, aParams->lambdamax);
  muse_geo_table_delete(geofinal);
  cpl_propertylist_delete(headfinal);

  return result != CPL_ERROR_NONE ? -1 : 0;
} /* muse_geometry_compute() */
