/* $Id: kmo_wave_cal.c,v 1.54 2013-09-17 08:54:03 aagudo Exp $
 *
 * This file is part of the KMOS Pipeline
 * Copyright (C) 2002,2003 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

/*
 * $Author: aagudo $
 * $Date: 2013-09-17 08:54:03 $
 * $Revision: 1.54 $
 * $Name: not supported by cvs2svn $
 */

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

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

#ifdef __USE_XOPEN2K
#include <stdlib.h>
#define GGG
#else
#define __USE_XOPEN2K /* to get the definition for setenv in stdlib.h */
#include <stdlib.h>
#undef __USE_XOPEN2K
#endif

#include <cpl.h>

#include "kmo_utils.h"
#include "kmo_functions.h"
#include "kmo_priv_wave_cal.h"
#include "kmo_priv_functions.h"
#include "kmo_cpl_extensions.h"
#include "kmo_dfs.h"
#include "kmo_error.h"
#include "kmo_constants.h"
#include "kmo_debug.h"

/*-----------------------------------------------------------------------------
 *                          Functions prototypes
 *----------------------------------------------------------------------------*/

static int kmo_wave_cal_create(cpl_plugin *);
static int kmo_wave_cal_exec(cpl_plugin *);
static int kmo_wave_cal_destroy(cpl_plugin *);
static int kmo_wave_cal(cpl_parameterlist *, cpl_frameset *);

/*-----------------------------------------------------------------------------
 *                          Static variables
 *----------------------------------------------------------------------------*/

static char kmo_wave_cal_description[] =
"This recipe creates the wavelength calibration frame needed for all three\n"
"detectors. It must be called after the kmo_flat recipe, which generates the\n"
"two spatial calibration frames needed in this recipe. As input a lamp-on frame,\n"
"a lamp-off frame, the spatial calibration frames and the list with the\n"
"reference arclines are required."
"An additional output frame is the resampled image of the reconstructed arc\n"
"frame. All slitlets of all IFUs are aligned one next to the other. This frame\n"
"serves for quality control. One can immediately see if the calibration was\n"
"successful.\n"
"The lists of reference arclines are supposed to contain the lines for both\n"
"available calibration arc-lamps, i.e. Argon and Neon. The list is supposed to\n"
"be a F2L KMOS FITS file with three columns:\n"
"1. Reference wavelength\n"
"2. Relative strength\n"
"3. String either containing “Ar” or “Ne”\n"
"The recipe extracts, based on the header keywords, either the applying argon\n"
"and/or neon emission lines. Below are the plots of the emission lines for both\n"
"argon and neon. The marked lines are the ones used for wavelength calibration.\n"
"\n"
"Furthermore frames can be provided for several rotator angles. In this case\n"
"the resulting calibration frames for each detector are repeatedly saved as \n"
"extension for every angle.\n"
"\n"
"BASIC PARAMETERS:\n"
"-----------------\n"
"--order\n"
"The polynomial order to use for the fit of the wavelength solution.\n"
"0: (default) The appropriate order is choosen automatically depending on the\n"
"waveband. Otherwise an order of 6 is recommended, except for IZ-band, there\n"
"order 4 should be used.\n"
"\n"
"ADVANCED PARAMETERS\n"
"-------------------\n"
"--b_samples\n"
"The number of samples in spectral direction for the reconstructed cube.\n"
"Ideally this number should be greater than 2048, the detector size.\n"
"\n"
"--b_start\n"
"--b_end\n"
"Used to define manually the start and end wavelength for the reconstructed\n"
"cube. By default the internally defined values are used.\n"
"\n"
"--suppress_extension\n"
"If set to TRUE, the arbitrary filename extensions are supressed. If multiple\n"
"products with the same category are produced, they will be numered consecutively\n"
"starting from 0.\n"
"\n"
"-------------------------------------------------------------------------------\n"
"  Input files:\n"
"\n"
"   DO                    KMOS                                                  \n"
"   category              Type   Explanation                    Required #Frames\n"
"   --------              -----  -----------                    -------- -------\n"
"   ARC_ON                RAW    Arclamp-on exposure                Y        >=1\n"
"   ARC_OFF               RAW    Arclamp-off exposure               Y          1\n"
"   XCAL                  F2D    x calibration frame                Y          1\n"
"   YCAL                  F2D    y calibration frame                Y          1\n"
"   ARC_LIST              F2L    List of arclines                   Y          1\n"
"   FLAT_EDGE             F2L    Fitted edge parameters             Y          1\n"
"   REF_LINES             F2L    Reference line table               Y          1\n"
"   WAVE_BAND             F2L    Table with start-/end-wavelengths  Y          1\n"
"\n"
"  Output files:\n"
"\n"
"   DO                    KMOS\n"
"   category              Type   Explanation\n"
"   --------              -----  -----------\n"
"   LCAL                  F2D    Wavelength calibration frame\n"
"                                (3 Extensions)\n"
"   DET_IMG_WAVE          F2D    reconstructed arclamp-on exposure\n"
"                                (4 extensions: 3 detector images + \n"
"                                the arclines list table)\n"
"-------------------------------------------------------------------------------\n"
"\n";

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

/**
 * @defgroup kmo_wave_cal kmo_wave_cal Create a calibration frame encoding the spectral position
 *
 * See recipe description for details.
 */

/**@{*/

/**
  @brief    Build the list of available plugins, for this module. 
  @param    list    the plugin list
  @return   0 if everything is ok, -1 otherwise

  Create the recipe instance and make it available to the application using the 
  interface. This function is exported.
 */
int cpl_plugin_get_info(cpl_pluginlist *list)
{
    cpl_recipe *recipe = cpl_calloc(1, sizeof *recipe);
    cpl_plugin *plugin = &recipe->interface;

    cpl_plugin_init(plugin,
                        CPL_PLUGIN_API,
                        KMOS_BINARY_VERSION,
                        CPL_PLUGIN_TYPE_RECIPE,
                        "kmo_wave_cal",
                        "Create a calibration frame encoding the spectral "
                        "position (i.e. wavelength) of each pixel on the "
                        "detector.",
                        kmo_wave_cal_description,
                        "Alex Agudo Berbel",
                        "kmos-spark@mpe.mpg.de",
                        kmos_get_license(),
                        kmo_wave_cal_create,
                        kmo_wave_cal_exec,
                        kmo_wave_cal_destroy);

    cpl_pluginlist_append(list, plugin);

    return 0;
}

/**
  @brief    Setup the recipe options    
  @param    plugin  the plugin
  @return   0 if everything is ok

  Defining the command-line/configuration parameters for the recipe.
 */
static int kmo_wave_cal_create(cpl_plugin *plugin)
{
    cpl_recipe *recipe;
    cpl_parameter *p;

    // Check that the plugin is part of a valid recipe
    if (cpl_plugin_get_type(plugin) == CPL_PLUGIN_TYPE_RECIPE) 
        recipe = (cpl_recipe *)plugin;
    else
        return -1;

    // Create the parameters list in the cpl_recipe object
    recipe->parameters = cpl_parameterlist_new();

    // Fill the parameters list
    p = cpl_parameter_new_value("kmos.kmo_wave_cal.order",
                                CPL_TYPE_INT,
                                "The polynomial order to use for the fit of "
                                "the wavelength solution. 0: (default) The "
                                "appropriate order is choosen automatically "
                                "depending on the waveband. Otherwise an order "
                                "of 6 is recommended, except for IZ-band, there "
                                "order 4 should be used",
                                "kmos.kmo_wave_cal",
                                0);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "order");
    cpl_parameter_disable(p, CPL_PARAMETER_MODE_ENV);
    cpl_parameterlist_append(recipe->parameters, p);

    p = cpl_parameter_new_value("kmos.kmo_wave_cal.dev_flip",
                                CPL_TYPE_BOOL,
                                "Set this parameter to TRUE if the wavelengths"
                                " are ascending on the detector from top to "
                                "bottom.",
                                "kmos.kmo_wave_cal",
                                FALSE);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "dev_flip");
    cpl_parameter_disable(p, CPL_PARAMETER_MODE_ENV);
    cpl_parameterlist_append(recipe->parameters, p);

    p = cpl_parameter_new_value("kmos.kmo_wave_cal.dev_disp",
                                CPL_TYPE_DOUBLE,
                                "The expected dispersion of the wavelength "
                                "in microns/pixel. If the default isn't "
                                "changed it will automatically be selected "
                                "upon header keywords.",
                                "kmos.kmo_wave_cal",
                                -1.0);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "dev_disp");
    cpl_parameter_disable(p, CPL_PARAMETER_MODE_ENV);
    cpl_parameterlist_append(recipe->parameters, p);

    /* --suppress_extension */
    p = cpl_parameter_new_value("kmos.kmo_wave_cal.suppress_extension",
                                CPL_TYPE_BOOL,
                                "Suppress arbitrary filename extension."
                                "(TRUE (apply) or FALSE (don't apply)",
                                "kmos.kmo_wave_cal",
                                FALSE);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "suppress_extension");
    cpl_parameter_disable(p, CPL_PARAMETER_MODE_ENV);
    cpl_parameterlist_append(recipe->parameters, p);

    // add parameters for band-definition
    kmo_band_pars_create(recipe->parameters,
                         "kmos.kmo_wave_cal");

    return 0;
}

/**
  @brief    Execute the plugin instance given by the interface
  @param    plugin  the plugin
  @return   0 if everything is ok
 */
static int kmo_wave_cal_exec(cpl_plugin *plugin)
{
    cpl_recipe  *recipe;

    // Get the recipe out of the plugin
    if (cpl_plugin_get_type(plugin) == CPL_PLUGIN_TYPE_RECIPE) 
        recipe = (cpl_recipe *)plugin;
    else return -1;

    return kmo_wave_cal(recipe->parameters, recipe->frames);
}

/**
  @brief    Destroy what has been created by the 'create' function
  @param    plugin  the plugin
  @return   0 if everything is ok
 */
static int kmo_wave_cal_destroy(cpl_plugin *plugin)
{
    cpl_recipe *recipe;

    // Get the recipe out of the plugin
    if (cpl_plugin_get_type(plugin) == CPL_PLUGIN_TYPE_RECIPE) 
        recipe = (cpl_recipe *)plugin;
    else return -1 ;

    cpl_parameterlist_delete(recipe->parameters);
    return 0 ;
}

/**
  @brief    Interpret the command line options and execute the data processing
  @param    parlist     the parameters list
  @param    frameset   the frames list
  @return   0 if everything is ok

  Possible _cpl_error_code_ set in this function:

    @li CPL_ERROR_ILLEGAL_INPUT      if operator not valid,
                                     if first operand not 3d or
                                     if second operand not valid
    @li CPL_ERROR_INCOMPATIBLE_INPUT if the dimensions of the two operands
                                     do not match
 */
static int kmo_wave_cal(cpl_parameterlist *parlist, cpl_frameset *frameset)
{
    cpl_image        *det_lamp_on                   = NULL,
                     *det_lamp_on_copy              = NULL,
                     *det_lamp_off                  = NULL,
                     *bad_pix_mask                  = NULL,
                     *xcal                          = NULL,
                     *ycal                          = NULL,
                     *lcal                          = NULL;

    cpl_image        **stored_lcal                  = NULL,
                     **stored_det_img               = NULL;

    cpl_frame        *frame                         = NULL;
    cpl_frameset     ** angle_frameset     = NULL;

    int              ret_val                        = 0,
                     nr_devices                     = 0,
                     nr_angles                      = 0,
                     nx                             = 0,
                     ny                             = 0,
                     nz                             = 0,
                     *stored_qc_arc_sat             = NULL,
                     flip_trace                     = FALSE,
                     fit_order_par                  = 0,
                     fit_order                      = 0,
                     ndit                           = 0,
                     suppress_extension             = FALSE,
                     line_estimate_method           = 2,
                     nr_sat                         = 0,
                     i = 0, j = 0, a = 0, ax = 0, x = 0, y = 0;
    float            *pbad_pix_mask                 = NULL;
    double           exptime                        = 0.0,
                     gain                           = 0.0,
                     *stored_qc_ar_eff              = NULL,
                     *stored_qc_ne_eff              = NULL,
                     disp                           = 0.0,
                     angle_found                    = 0.0;

    cpl_table        *arclines                      = NULL,
                     *reflines                      = NULL,
                     ***edge_table                  = NULL;

    cpl_propertylist *main_header                   = NULL,
                     **stored_sub_headers_lcal      = NULL,
                     **stored_sub_headers_det_img   = NULL,
                     *qc_header                     = NULL,
                     *tmp_header                    = NULL,
                     *header                        = NULL;

    cpl_array        **unused_ifus_before           = NULL,
                     **unused_ifus_after            = NULL;

    cpl_bivector     *lines                         = NULL;

    main_fits_desc   desc1,
                     desc2;

    char             *extname                       = NULL,
                     filename_lcal[256],
                     filename_det_img[256],
                     *suffix                        = NULL,
                     *fn_suffix                     = NULL,
                     tmpstr[256],
                     **filter_ids                   = NULL,
                     *readmode                      = NULL,
                     *str_line_estimate_method      = NULL,
                     *last_env                      = NULL;

    const char       *tmp_str                       = NULL;

    cpl_error_code   err                            = CPL_ERROR_NONE;

    enum lampConfiguration lamp_config;

    KMO_TRY
    {
        kmo_init_fits_desc(&desc1);
        kmo_init_fits_desc(&desc2);

        str_line_estimate_method = getenv("KMO_WAVE_LINE_ESTIMATE");
        if (str_line_estimate_method != NULL) {
            line_estimate_method = atoi(str_line_estimate_method);
        }
        cpl_msg_debug(cpl_func, "Line estimation method: %d\n",
                                line_estimate_method);

// #############################################################################
// ###           check inputs
// #############################################################################
        KMO_TRY_ASSURE((parlist != NULL) &&
                       (frameset != NULL),
                       CPL_ERROR_NULL_INPUT,
                       "Not all input data is provided!");

        KMO_TRY_ASSURE(cpl_frameset_count_tags(frameset, ARC_ON) >= 1,
                       CPL_ERROR_NULL_INPUT,
                       "At least one ARC_ON frame is required!");

        KMO_TRY_ASSURE(cpl_frameset_count_tags(frameset, ARC_OFF) >= 1,
                       CPL_ERROR_NULL_INPUT,
                       "Exactly one ARC_OFF frame is required!");

        if (cpl_frameset_count_tags(frameset, ARC_OFF) > 1) {
            cpl_msg_warning("", "Exactly one ARC_OFF frame is required. "
                                "Just the first frame will be used");
        }

        KMO_TRY_ASSURE(cpl_frameset_count_tags(frameset, XCAL) == 1,
                       CPL_ERROR_FILE_NOT_FOUND,
                       "Exactly one XCAL frame is required!");

        KMO_TRY_ASSURE(cpl_frameset_count_tags(frameset, YCAL) == 1,
                       CPL_ERROR_FILE_NOT_FOUND,
                       "Exactly one YCAL frame is required!");

        KMO_TRY_ASSURE(cpl_frameset_count_tags(frameset, ARC_LIST) == 1,
                       CPL_ERROR_FILE_NOT_FOUND,
                       "Exactly one ARC_LIST frame is required!");

        KMO_TRY_ASSURE(cpl_frameset_count_tags(frameset, FLAT_EDGE) == 1,
                       CPL_ERROR_FILE_NOT_FOUND,
                       "Exactly one FLAT_EDGE frame is required!");

        KMO_TRY_ASSURE(cpl_frameset_count_tags(frameset, WAVE_BAND) == 1,
                       CPL_ERROR_FILE_NOT_FOUND,
                       "Exactly one WAVE_BAND frame is required!");

        if (line_estimate_method == 2) {
            KMO_TRY_ASSURE(cpl_frameset_count_tags(frameset, REF_LINES) == 1,
                           CPL_ERROR_FILE_NOT_FOUND,
                           "Exactly one REF_LINES frame is required!");
        }
        
        KMO_TRY_ASSURE(kmo_dfs_set_groups(frameset, "kmo_wave_cal") == 1,
                       CPL_ERROR_ILLEGAL_INPUT,
                       "Cannot identify RAW and CALIB frames!");

        //
        // ------------ get parameters ------------
        //
        cpl_msg_info("", "--- Parameter setup for kmo_wave_cal ------");

        fit_order_par = kmo_dfs_get_parameter_int(parlist,
                                              "kmos.kmo_wave_cal.order");
        KMO_TRY_CHECK_ERROR_STATE();
        KMO_TRY_EXIT_IF_ERROR(
            kmo_dfs_print_parameter_help(parlist, "kmos.kmo_wave_cal.order"));
        KMO_TRY_ASSURE((fit_order_par >= 0) &&
                       (fit_order_par <= 8),
                       CPL_ERROR_ILLEGAL_INPUT,
                       "order must be between 1 and 8!");

        flip_trace = kmo_dfs_get_parameter_bool(parlist,
                                           "kmos.kmo_wave_cal.dev_flip");
        KMO_TRY_CHECK_ERROR_STATE();
        KMO_TRY_EXIT_IF_ERROR(
            kmo_dfs_print_parameter_help(parlist, "kmos.kmo_wave_cal.dev_flip"));
        KMO_TRY_ASSURE((flip_trace == TRUE) ||
                       (flip_trace == FALSE),
                       CPL_ERROR_ILLEGAL_INPUT,
                       "flip must be TRUE or FALSE!");

        disp = kmo_dfs_get_parameter_double(parlist,
                                           "kmos.kmo_wave_cal.dev_disp");
        KMO_TRY_CHECK_ERROR_STATE();
        KMO_TRY_EXIT_IF_ERROR(
            kmo_dfs_print_parameter_help(parlist, "kmos.kmo_wave_cal.dev_disp"));

        KMO_TRY_ASSURE((disp > 0.0) || (disp == -1.0),
                       CPL_ERROR_ILLEGAL_INPUT,
                       "dispersion must be greater than 0.0!");

        suppress_extension = kmo_dfs_get_parameter_bool(parlist,
                                          "kmos.kmo_wave_cal.suppress_extension");
        KMO_TRY_CHECK_ERROR_STATE();
        KMO_TRY_EXIT_IF_ERROR(
            kmo_dfs_print_parameter_help(parlist, "kmos.kmo_wave_cal.suppress_extension"));

        KMO_TRY_ASSURE((suppress_extension == TRUE) || (suppress_extension == FALSE),
                       CPL_ERROR_ILLEGAL_INPUT,
                       "suppress_extension must be TRUE or FALSE!");

        kmo_band_pars_load(parlist, "kmos.kmo_wave_cal");

        cpl_msg_info("", "-------------------------------------------");

        //
        // ------------ check EXPTIME, NDIT, READMODE and lamps ------------
        //

        // check ARC_OFF
        KMO_TRY_EXIT_IF_NULL(
            frame = kmo_dfs_get_frame(frameset, ARC_OFF));

        main_header = kmclipm_propertylist_load(cpl_frame_get_filename(frame), 0);

        ndit = cpl_propertylist_get_int(main_header, NDIT);
        KMO_TRY_CHECK_ERROR_STATE("NDIT keyword in main header "
                                  "missing!");

        exptime = cpl_propertylist_get_double(main_header, EXPTIME);
        KMO_TRY_CHECK_ERROR_STATE("EXPTIME keyword in main header "
                                  "missing!");

        readmode = cpl_strdup(cpl_propertylist_get_string(main_header, READMODE));
        KMO_TRY_CHECK_ERROR_STATE("ESO DET READ CURNAME keyword in main "
                                  "header missing!");

        desc1 = kmo_identify_fits_header(
                    cpl_frame_get_filename(frame));
        KMO_TRY_CHECK_ERROR_STATE_MSG("ARC_OFF frame doesn't seem to "
                                      "be in KMOS-format!");

        KMO_TRY_ASSURE(desc1.fits_type == raw_fits,
                       CPL_ERROR_ILLEGAL_INPUT,
                       "ARC_OFF frame hasn't correct data type "
                       "(%s must be a raw, unprocessed file)!",
                       cpl_frame_get_filename(frame));

        nx = desc1.naxis1;
        ny = desc1.naxis2;
        nz = desc1.naxis3;
        nr_devices = desc1.nr_ext;

        // assure that flat lamps are off
        KMO_TRY_ASSURE((kmo_check_lamp(main_header, INS_LAMP3_ST) == FALSE) &&
                       (kmo_check_lamp(main_header, INS_LAMP4_ST) == FALSE),
                       CPL_ERROR_ILLEGAL_INPUT,
                       "Flat lamps must be switched off (%s)!",
                       cpl_frame_get_filename(frame));

        // check if arc lamps are off (or at least blocked by filter wheel)
        if ((kmo_check_lamp(main_header, INS_LAMP1_ST) == TRUE) ||
            (kmo_check_lamp(main_header, INS_LAMP2_ST) == TRUE))
        {
            if (!(strcmp(cpl_propertylist_get_string(main_header, "ESO INS FILT1 ID"), "Block") == 0) ||
                !(strcmp(cpl_propertylist_get_string(main_header, "ESO INS FILT3 ID"), "Block") == 0) ||
                !(strcmp(cpl_propertylist_get_string(main_header, "ESO INS FILT3 ID"), "Block") == 0))
            {
                cpl_msg_warning("","At least one arc lamp is on! Check if the lamps are blocked!");
            }
        }
        cpl_propertylist_delete(main_header); main_header = NULL;

        // check REF_LINES
        if (line_estimate_method == 2) {
            KMO_TRY_EXIT_IF_NULL(
                frame = kmo_dfs_get_frame(frameset, REF_LINES));
            desc2 = kmo_identify_fits_header(
                        cpl_frame_get_filename(frame));
            KMO_TRY_CHECK_ERROR_STATE_MSG("REF_LINES frame doesn't seem to "
                                          "be in KMOS-format!");

            KMO_TRY_ASSURE(desc2.fits_type == f2l_fits,
                           CPL_ERROR_ILLEGAL_INPUT,
                           "REF_LINES frame hasn't correct frame type "
                           "(%s must be a F2L frame)!",
                           cpl_frame_get_filename(frame));
            kmo_free_fits_desc(&desc2);
        }

        // check ARC_ON
        KMO_TRY_EXIT_IF_NULL(
            frame = kmo_dfs_get_frame(frameset, ARC_ON));

        while (frame != NULL) {
            main_header = kmclipm_propertylist_load(cpl_frame_get_filename(frame), 0);

            KMO_TRY_ASSURE(cpl_propertylist_get_int(main_header, NDIT) == ndit,
                    CPL_ERROR_ILLEGAL_INPUT,
                    "NDIT isn't the same for all frames: (is %d and %d).",
                    cpl_propertylist_get_int(main_header, NDIT), ndit);

            KMO_TRY_ASSURE(cpl_propertylist_get_double(main_header, EXPTIME) == exptime,
                    CPL_ERROR_ILLEGAL_INPUT,
                    "EXPTIME isn't the same for all frames: (is %g and %g).",
                    cpl_propertylist_get_double(main_header, EXPTIME), exptime);

            KMO_TRY_ASSURE(strcmp(cpl_propertylist_get_string(main_header, READMODE), readmode) == 0,
                    CPL_ERROR_ILLEGAL_INPUT,
                    "ESO DET READ CURNAME isn't the same for all frames: (is %s and %s).",
                    cpl_propertylist_get_string(main_header, READMODE), readmode);

            desc2 = kmo_identify_fits_header(
                    cpl_frame_get_filename(frame));
            KMO_TRY_CHECK_ERROR_STATE_MSG("ARC_ON frame doesn't seem to "
                    "be in KMOS-format!");

            KMO_TRY_ASSURE(desc2.fits_type == raw_fits,
                    CPL_ERROR_ILLEGAL_INPUT,
                    "ARC_ON frame hasn't correct data type "
                    "(%s must be a raw, unprocessed file)!",
                    cpl_frame_get_filename(frame));

            KMO_TRY_ASSURE((desc2.naxis1 == nx) &&
                    (desc2.naxis2 == ny) &&
                    (desc2.naxis3 == nz),
                    CPL_ERROR_ILLEGAL_INPUT,
                    "ARC_ON frame hasn't correct dimensions! (x,y): (%d,%d) "
                    "vs (%d,%d)", desc2.naxis1, desc2.naxis2, nx, ny);


            // assure that flat lamp is off (LAMP3 and 4)
            // and that either arc lamp is on (LAMP1 and 2)
            KMO_TRY_ASSURE(((kmo_check_lamp(main_header, INS_LAMP1_ST) == TRUE) ||
                    (kmo_check_lamp(main_header, INS_LAMP2_ST) == TRUE)) &&
                    (kmo_check_lamp(main_header, INS_LAMP3_ST) == FALSE) &&
                    (kmo_check_lamp(main_header, INS_LAMP4_ST) == FALSE),
                    CPL_ERROR_ILLEGAL_INPUT,
                    "Lamp1 or Lamp2 must be switched on, 3 and 4 must be "
                    "off for the ARC_ON frame");

            frame = kmo_dfs_get_frame(frameset, NULL);
            KMO_TRY_CHECK_ERROR_STATE();

            kmo_free_fits_desc(&desc2);
            kmo_init_fits_desc(&desc2);
            cpl_propertylist_delete(main_header); main_header = NULL;
        }

        // load first ARC_ON main header
        KMO_TRY_EXIT_IF_NULL(
            frame = kmo_dfs_get_frame(frameset, ARC_ON));
        KMO_TRY_EXIT_IF_NULL(
            main_header = kmclipm_propertylist_load(cpl_frame_get_filename(frame), 0));

        // check FLAT_EDGE
        KMO_TRY_EXIT_IF_NULL(
            frame = kmo_dfs_get_frame(frameset, FLAT_EDGE));

        desc2 = kmo_identify_fits_header(cpl_frame_get_filename(frame));
        KMO_TRY_CHECK_ERROR_STATE();

        KMO_TRY_ASSURE((desc2.nr_ext % 24== 0) &&
                       (desc2.fits_type == f2l_fits),
                       CPL_ERROR_ILLEGAL_INPUT,
                       "FLAT_EDGE isn't in the correct format!!!");

        kmo_free_fits_desc(&desc2);
        kmo_init_fits_desc(&desc2);

        //
        // ------------ check filter_id, grating_id and rotator offset ---------
        // assure that filters, grating and rotation offsets match for
        // ARC_ON, XCAL and YCAL
        // check if filter_id, grating_id and rotator offset match for all
        // detectors
        //
        KMO_TRY_EXIT_IF_ERROR(
            kmo_check_frame_setup(frameset, ARC_ON, XCAL,
                                       TRUE, FALSE, FALSE));
        KMO_TRY_EXIT_IF_ERROR(
            kmo_check_frame_setup(frameset, ARC_ON, YCAL,
                                       TRUE, FALSE, FALSE));

        KMO_TRY_EXIT_IF_ERROR(
            kmo_check_frame_setup_md5_xycal(frameset));

        strcpy(filename_lcal, LCAL);
        strcpy(filename_det_img, DET_IMG_WAVE);

        KMO_TRY_EXIT_IF_NULL(
            frame = kmo_dfs_get_frame(frameset, XCAL));
        KMO_TRY_EXIT_IF_NULL(
            suffix = kmo_dfs_get_suffix(frame, TRUE, FALSE));

        cpl_msg_info("", "Detected instrument setup:   %s", suffix+1);
        cpl_msg_info("", "(grating 1, 2 & 3)");

        // setup lamp config
        if ((kmo_check_lamp(main_header, INS_LAMP1_ST) == TRUE) &&
            (kmo_check_lamp(main_header, INS_LAMP2_ST) == FALSE))
        {
            lamp_config = ARGON;
            strcpy(tmpstr, "Argon");
        } else if ((kmo_check_lamp(main_header, INS_LAMP1_ST) == FALSE) &&
                   (kmo_check_lamp(main_header, INS_LAMP2_ST) == TRUE))
        {
           lamp_config = NEON;
           strcpy(tmpstr, "Neon");
        } else if ((kmo_check_lamp(main_header, INS_LAMP1_ST) == TRUE) &&
                   (kmo_check_lamp(main_header, INS_LAMP2_ST) == TRUE))
        {
           lamp_config = ARGON_NEON;
           strcpy(tmpstr, "Argon + Neon");
        }

        cpl_msg_info("", "Detected arc lamp configuration: %s", tmpstr);

        //assert that filter and grating match for each detector
        // filter/grating can be different for each detector
        KMO_TRY_EXIT_IF_NULL(
            filter_ids =  kmo_get_filter_setup(main_header, nr_devices, TRUE));

        //
        // ---- scan for rotator angles
        //
        #define ANGLE_DIM 360
        int rotang_found[ANGLE_DIM];
        int rotang_cnt[ANGLE_DIM];
        for (i = 0; i < ANGLE_DIM; i++) {
            rotang_found[i] = 0;
            rotang_cnt[i] = 0;
        }
        KMO_TRY_EXIT_IF_NULL(
            frame = kmo_dfs_get_frame(frameset, ARC_ON));
        while (frame != NULL) {
            header = kmclipm_propertylist_load(cpl_frame_get_filename(frame), 0);
            if (cpl_propertylist_has(header, ROTANGLE)) {
                int rot_angle = (int) rint(cpl_propertylist_get_double(header, ROTANGLE));
                if (rot_angle < 0) {
                    rot_angle += 360;
                }
                if (rot_angle < 360 && rot_angle >= 0) {
                    rotang_cnt[rot_angle]++;
//                    char * tag = cpl_sprintf("FLAT_ON_%3.3d",rot_angle);
//                    KMO_TRY_EXIT_IF_ERROR(
//                            cpl_frame_set_tag(frame, tag));
//                    cpl_free(tag);
                }
            } else {
                cpl_msg_warning("","File %s has no keyword \"ROTANGLE\"",
                        cpl_frame_get_filename(frame));
            }

            cpl_propertylist_delete(header); header = NULL;
            frame = kmo_dfs_get_frame(frameset, NULL);
            KMO_TRY_CHECK_ERROR_STATE();
        }
        for (ax = 0; ax < ANGLE_DIM; ax++) {
            if (rotang_cnt[ax] != 0) {
                if (rotang_cnt[ax] == 1 ) {
                    cpl_msg_info("","Found %d frame with rotator angle %d",rotang_cnt[ax],ax);
                } else {
                    cpl_msg_warning("","Found %d frames with rotator angle %d but only one will be used",
                                    rotang_cnt[ax],ax);
                }
                rotang_found[nr_angles] = ax;
                nr_angles++;
            }
        }

        KMO_TRY_EXIT_IF_NULL (
            angle_frameset = (cpl_frameset **) cpl_malloc(nr_angles * sizeof(cpl_frameset*)));
        for (i = 0; i < nr_angles; i++) {
            angle_frameset[i] = cpl_frameset_new();
        }

        KMO_TRY_EXIT_IF_NULL(
            frame = kmo_dfs_get_frame(frameset, ARC_ON));
        while (frame != NULL) {
            header = kmclipm_propertylist_load(cpl_frame_get_filename(frame), 0);
            if (cpl_propertylist_has(header, ROTANGLE)) {
                int rot_angle = (int) rint(cpl_propertylist_get_double(header, ROTANGLE));
                if (rot_angle < 0) {
                    rot_angle += 360;
                }
                int ix = -1;
                for (ix = 0; ix<nr_angles; ix++) {
                    if (rotang_found[ix] == rot_angle) {
                        break;
                    }
                }
                if (ix<nr_angles) {
                    KMO_TRY_EXIT_IF_ERROR(
                        cpl_frameset_insert(angle_frameset[ix], cpl_frame_duplicate(frame)));
                }
            }

            cpl_propertylist_delete(header); header = NULL;
            frame = kmo_dfs_get_frame(frameset, NULL);
            KMO_TRY_CHECK_ERROR_STATE();
        }

// #############################################################################
// ###           allocate temporary memory
// #############################################################################
        // allocate here a edge table structure for all detectors
        KMO_TRY_EXIT_IF_NULL(
            edge_table = (cpl_table***)cpl_calloc(nr_devices,
                                                  sizeof(cpl_table**)));
        for (i = 0; i < nr_devices; i++) {
            KMO_TRY_EXIT_IF_NULL(
                edge_table[i] = (cpl_table**)cpl_calloc(KMOS_IFUS_PER_DETECTOR,
                                                        sizeof(cpl_table*)));
        }

        // the frames have to be stored temporarily because the QC parameters
        // for the main header are calculated from each detector. So they can be
        // stored only when all detectors are processed
        KMO_TRY_EXIT_IF_NULL(
            stored_lcal = (cpl_image**)cpl_calloc(nr_devices * nr_angles,
                                                    sizeof(cpl_image*)));
        KMO_TRY_EXIT_IF_NULL(
            stored_det_img = (cpl_image**)cpl_calloc(nr_devices * nr_angles,
                                                    sizeof(cpl_image*)));
        KMO_TRY_EXIT_IF_NULL(
            stored_sub_headers_lcal = (cpl_propertylist**)cpl_calloc(nr_devices * nr_angles,
                                                    sizeof(cpl_propertylist*)));
        KMO_TRY_EXIT_IF_NULL(
            stored_sub_headers_det_img = (cpl_propertylist**)cpl_calloc(nr_devices * nr_angles,
                                                    sizeof(cpl_propertylist*)));
        KMO_TRY_EXIT_IF_NULL(
            stored_qc_arc_sat =
                            (int*)cpl_calloc(nr_devices, nr_angles * sizeof(int)));
        KMO_TRY_EXIT_IF_NULL(
            stored_qc_ar_eff =
                            (double*)cpl_calloc(nr_devices, nr_angles * sizeof(double)));
        KMO_TRY_EXIT_IF_NULL(
            stored_qc_ne_eff =
                            (double*)cpl_calloc(nr_devices, nr_angles * sizeof(double)));

// #############################################################################
// ###           process data
// #############################################################################
        // load arclines and reference lines
        KMO_TRY_EXIT_IF_NULL(
            frame = kmo_dfs_get_frame(frameset, ARC_LIST));

        // check if ARC_LIST is the filter_id-one
        KMO_TRY_EXIT_IF_NULL(
            tmp_header = kmclipm_propertylist_load(cpl_frame_get_filename(frame), 0));
        KMO_TRY_EXIT_IF_NULL(
            tmp_str = cpl_propertylist_get_string(tmp_header, FILT_ID));
        KMO_TRY_ASSURE(strcmp(filter_ids[0], tmp_str) == 0,
                       CPL_ERROR_ILLEGAL_INPUT,
                       "ARC_LIST model must have primary "
                       "keyword '%s' equal '%s'!!!", FILT_ID, filter_ids[0]);
        cpl_propertylist_delete(tmp_header); tmp_header = NULL;

        desc2 = kmo_identify_fits_header(cpl_frame_get_filename(frame));
        KMO_TRY_CHECK_ERROR_STATE();

        KMO_TRY_ASSURE((desc2.nr_ext == 1) &&
                       (desc2.fits_type == f2l_fits),
                       CPL_ERROR_ILLEGAL_INPUT,
                       "ARC_LIST isn't in the correct format!!!");
        kmo_free_fits_desc(&desc2);
        kmo_init_fits_desc(&desc2);

        KMO_TRY_EXIT_IF_NULL(
            arclines = kmo_dfs_load_table(frameset, ARC_LIST, 1, 0));

        KMO_TRY_EXIT_IF_NULL(
            lines = kmo_get_lines(arclines, lamp_config));

        cpl_msg_info("", "Nr. of lines in arclist for this configuration: %lld",
                     cpl_bivector_get_size(lines));

        if (line_estimate_method == 2) {
            KMO_TRY_EXIT_IF_NULL(
                reflines = kmo_dfs_load_table(frameset, REF_LINES, 1, 0));
        }

        // check which IFUs are active for all FLAT frames
        KMO_TRY_EXIT_IF_NULL(
            unused_ifus_before = kmo_get_unused_ifus(frameset, 0, 0));

        KMO_TRY_EXIT_IF_NULL(
            unused_ifus_after = kmo_duplicate_unused_ifus(unused_ifus_before));

        kmo_print_unused_ifus(unused_ifus_before, FALSE);

        // make sure no reconstruction lookup table (LUT) is used
        if (getenv("KMCLIPM_PRIV_RECONSTRUCT_LUT_MODE") != NULL) {
            last_env = getenv("KMCLIPM_PRIV_RECONSTRUCT_LUT_MODE");
        }
        setenv("KMCLIPM_PRIV_RECONSTRUCT_LUT_MODE","NONE",1);

        //
        // ------------ loop all rotator angles and detectors ------------
        //
        for (a = 0; a < nr_angles; a++) {
            cpl_msg_info("","Processing rotator angle %d -> %d degree", a,rotang_found[a]);

            for (i = 1; i <= nr_devices; i++) {
                // use loop below for line identification
                cpl_msg_info("","Processing detector No. %d", i);

                int sx = a * nr_devices + (i - 1);

                // load edge parameters
                KMO_TRY_EXIT_IF_NULL(
                    frame = kmo_dfs_get_frame(frameset, FLAT_EDGE));

                for (j = 0; j < KMOS_IFUS_PER_DETECTOR; j++) {
                    edge_table[i-1][j] = kmclipm_cal_table_load(cpl_frame_get_filename(frame),
                                                                (i-1) * KMOS_IFUS_PER_DETECTOR + j + 1,
                                                                rotang_found[a], 0, &angle_found);
                    if (cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT) {
                        // IFU is inactive: proceed
                        cpl_error_reset();
                    }
                }

                if (fit_order_par == 0) {
                    // set default fit orders for the different bands
                    if ((strcmp(filter_ids[i-1], "H") == 0) ||
                        (strcmp(filter_ids[i-1], "K") == 0) ||
                        (strcmp(filter_ids[i-1], "YJ") == 0))
                    {
                        fit_order = 6;
                    } else if (strcmp(filter_ids[i-1], "IZ") == 0) {
                        fit_order = 4;
                    } else if (strcmp(filter_ids[i-1], "HK") == 0) {
                        fit_order = 5;
                    }
                    cpl_msg_info("", "Order of wavelength spectrum fit for %s-band: %d",
                                 filter_ids[i-1],
                                 fit_order);
                } else {
                    fit_order = fit_order_par;
                }

                // lamp_on
                KMO_TRY_EXIT_IF_NULL(
                    frame = kmo_dfs_get_frame(angle_frameset[a], ARC_ON));

                // if sat_mode is set to TRUE here, then the calculation of the
                // saturated pixels has to be updated like in kmo_flat-recipe
                KMO_TRY_EXIT_IF_NULL(
                    det_lamp_on = kmo_dfs_load_image_frame(frame, i, FALSE, TRUE, &nr_sat));

                // count saturated pixels for each detector
                if (strcmp(cpl_propertylist_get_string(main_header, READMODE), "Nondest") == 0) {
                    // NDR: non-destructive readout mode
                    stored_qc_arc_sat[sx] = nr_sat;
                } else {
                    // normal readout mode
                    stored_qc_arc_sat[sx] = kmo_image_get_saturated(det_lamp_on,
                                                                    KMO_FLAT_SATURATED);
                }
                KMO_TRY_CHECK_ERROR_STATE();


                KMO_TRY_EXIT_IF_NULL(
                    det_lamp_on_copy = cpl_image_duplicate(det_lamp_on));

                // lamp_off
                KMO_TRY_EXIT_IF_NULL(
                    frame = kmo_dfs_get_frame(frameset, ARC_OFF));

                KMO_TRY_EXIT_IF_NULL(
                    det_lamp_off = kmo_dfs_load_image_frame(frame, i, FALSE, FALSE, NULL));

                //
                // process imagelist
                //
                KMO_TRY_CHECK_ERROR_STATE();

                // subtract lamp_off from lamp_on
                KMO_TRY_EXIT_IF_ERROR(
                    cpl_image_subtract(det_lamp_on, det_lamp_off));

                // load flat calibration frames
                KMO_TRY_EXIT_IF_NULL(
                    xcal = kmo_dfs_load_cal_image(frameset, XCAL, i, 0,
                                                  (double) rotang_found[a], FALSE,
                                                  NULL, &angle_found, -1, 0, 0));

                KMO_TRY_EXIT_IF_NULL(
                    ycal = kmo_dfs_load_cal_image(frameset, YCAL, i, 0,
                                                  (double) rotang_found[a], FALSE,
                                                  NULL,  &angle_found, -1, 0, 0));

                // load bad pixel mask from XCAL and set NaNs to 0 and all other values to 1
                KMO_TRY_EXIT_IF_NULL(
                    bad_pix_mask = cpl_image_duplicate(xcal));

                KMO_TRY_EXIT_IF_NULL(
                    pbad_pix_mask = cpl_image_get_data_float(bad_pix_mask));
                for (x = 0; x < nx; x++) {
                    for (y = 0; y < ny; y++) {
                        if (isnan(pbad_pix_mask[x+nx*y])) {
                            pbad_pix_mask[x+nx*y] = 0.;
                        } else {
                            pbad_pix_mask[x+nx*y] = 1.;
                        }
                    }
                }

                // calculate spectral curvature here
                err = kmo_calc_wave_calib(det_lamp_on,
                                          bad_pix_mask,
                                          xcal,
                                          ycal,
                                          filter_ids[i-1],
                                          lamp_config,
                                          i,
                                          unused_ifus_after[i-1],
                                          edge_table[i-1],
                                          lines,
                                          reflines,
                                          disp,
                                          &lcal,
                                          &(stored_qc_ar_eff[sx]),
                                          &(stored_qc_ne_eff[sx]),
                                          flip_trace,
                                          fit_order,
                                          line_estimate_method);

                if (err == CPL_ERROR_NONE) {
                    // calculate QC parameters
                    if (stored_qc_ar_eff[sx] != -1.0) {
                        stored_qc_ar_eff[sx] /= exptime;
                    }
                    if (stored_qc_ne_eff[sx] != -1.0) {
                        stored_qc_ne_eff[sx] /= exptime;
                    }

                    // apply the badpixel mask to the produced frame

                    KMO_TRY_EXIT_IF_ERROR(
                        cpl_image_multiply(lcal, bad_pix_mask));

                    KMO_TRY_EXIT_IF_ERROR(
                        kmo_image_reject_from_mask(lcal, bad_pix_mask));

                    // store flat frames, badpixel mask and calibration frames
                    stored_lcal[sx] = lcal;
                } else if (err == CPL_ERROR_UNSPECIFIED) {
                    // all IFUs seem to be deativated, continue processing
                    // just save empty frame
                    cpl_error_reset();
                    stored_lcal[sx] = cpl_image_new(nx, ny, CPL_TYPE_FLOAT);
                    kmo_image_fill(stored_lcal[sx], 0.0);
                } else {
                    cpl_msg_warning(cpl_func,
                                    "Couldn't identify any lines! Is the line "
                                    "list defined correctly?");
                    cpl_msg_warning(cpl_func,
                                    "Band defined in header of detector %d: %s",
                                    i, filter_ids[i-1]);
                    cpl_msg_warning(cpl_func,
                                    "Arc line file defined: %s",
                                    cpl_frame_get_filename(
                                          kmo_dfs_get_frame(frameset, ARC_LIST)));
                    cpl_error_reset();
                }


                //
                // create reconstructed and resampled arc frame
                //
                stored_det_img[sx] = kmo_reconstructed_arc_image(frameset,
                                                                 det_lamp_on_copy,
                                                                 det_lamp_off,
                                                                 xcal,
                                                                 ycal,
                                                                 stored_lcal[sx],
                                                                 unused_ifus_after[i-1],
                                                                 flip_trace,
                                                                 i,
                                                                 suffix,
                                                                 filter_ids[i-1],
                                                                 lamp_config,
                                                                 &qc_header);
                if (cpl_error_get_code() != CPL_ERROR_NONE) {
                    // couldn't reconstruct
                    cpl_msg_error("","Couldn't reconstruct IFUs on detector %d", i);
                    cpl_error_reset();
                }

                // load and update sub_header with QC parameters
                KMO_TRY_EXIT_IF_NULL(
                    stored_sub_headers_lcal[sx] = kmo_dfs_load_sub_header(frameset,
                                                                          ARC_ON, i,
                                                                          FALSE));

                // update EXTNAME
                KMO_TRY_EXIT_IF_NULL(
                    extname = kmo_extname_creator(detector_frame, i, EXT_DATA));
                KMO_TRY_EXIT_IF_ERROR(
                    kmclipm_update_property_string(stored_sub_headers_lcal[sx],
                                                   EXTNAME,
                                                   extname,
                                                   "FITS extension name"));
                cpl_free(extname); extname = NULL;

                KMO_TRY_EXIT_IF_ERROR(
                    kmclipm_update_property_int(stored_sub_headers_lcal[sx],
                                                EXTVER,
                                                sx+1,
                                                "FITS extension ver"));

                // add first QC parameters
                KMO_TRY_EXIT_IF_ERROR(
                    kmclipm_update_property_int(stored_sub_headers_lcal[sx],
                                                QC_ARC_SAT,
                                                stored_qc_arc_sat[sx],
                                                "[] nr. saturated pixels of arc exp."));

                gain = kmo_dfs_get_property_double(stored_sub_headers_lcal[sx], GAIN);
                KMO_TRY_CHECK_ERROR_STATE_MSG(
                    "GAIN-keyword in fits-header is missing!");

                if (stored_qc_ar_eff[sx] != -1.0) {
                    KMO_TRY_EXIT_IF_ERROR(
                        kmclipm_update_property_double(stored_sub_headers_lcal[sx],
                                                       QC_ARC_AR_EFF,
                                                       stored_qc_ar_eff[sx]/gain,
                                                       "[e-/s] Argon lamp efficiency"));
                }

                if (stored_qc_ne_eff[sx] != -1.0) {
                    KMO_TRY_EXIT_IF_ERROR(
                        kmclipm_update_property_double(stored_sub_headers_lcal[sx],
                                                       QC_ARC_NE_EFF,
                                                       stored_qc_ne_eff[sx]/gain,
                                                       "[e-/s] Neon lamp efficiency"));
                }

                KMO_TRY_EXIT_IF_ERROR(
                    kmclipm_update_property_double(stored_sub_headers_lcal[sx],
                                                   CAL_ROTANGLE,
                                                   ((double) rotang_found[a]),
                                                   "[deg] Rotator relative to nasmyth"));

                // append QC parameters
                KMO_TRY_EXIT_IF_ERROR(
                    cpl_propertylist_append(stored_sub_headers_lcal[sx],
                                            qc_header));
                cpl_propertylist_delete(qc_header); qc_header = NULL;

                KMO_TRY_EXIT_IF_NULL(
                    stored_sub_headers_det_img[sx] = cpl_propertylist_duplicate(
                                                     stored_sub_headers_lcal[sx]));

                cpl_propertylist_erase(stored_sub_headers_lcal[sx], CRVAL1);
                cpl_propertylist_erase(stored_sub_headers_lcal[sx], CRVAL2);
                cpl_propertylist_erase(stored_sub_headers_lcal[sx], CTYPE1);
                cpl_propertylist_erase(stored_sub_headers_lcal[sx], CTYPE2);
                cpl_propertylist_erase(stored_sub_headers_lcal[sx], CDELT1);
                cpl_propertylist_erase(stored_sub_headers_lcal[sx], CDELT2);

// leave keywords in for proper fitsverify output
//                cpl_propertylist_erase(stored_sub_headers_lcal[sx], CRPIX1);
//                cpl_propertylist_erase(stored_sub_headers_lcal[sx], CRPIX2);
//                if (cpl_propertylist_has(stored_sub_headers_lcal[sx], CRPIX1)) {
//                    cpl_propertylist_erase(stored_sub_headers_lcal[sx], CRPIX1);
//                }
//                if (cpl_propertylist_has(stored_sub_headers_lcal[sx], CRPIX2)) {
//                    cpl_propertylist_erase(stored_sub_headers_lcal[sx], CRPIX2);
//                }

                // free memory
                cpl_image_delete(det_lamp_on); det_lamp_on = NULL;
                cpl_image_delete(det_lamp_on_copy); det_lamp_on_copy = NULL;
                cpl_image_delete(det_lamp_off); det_lamp_off = NULL;
                cpl_image_delete(bad_pix_mask); bad_pix_mask = NULL;
                cpl_image_delete(xcal); xcal = NULL;
                cpl_image_delete(ycal); ycal = NULL;

                for (j = 0; j < KMOS_IFUS_PER_DETECTOR; j++) {
                    cpl_table_delete(edge_table[i-1][j]); edge_table[i-1][j] = NULL;
                }
            } // for i devices
        } // for a angles

        if (line_estimate_method == 2) {
            cpl_table_delete(reflines); reflines = NULL;
        }
// ###########################################################################
// ###           QC parameters & saving
// ###########################################################################
        cpl_msg_info("","Saving data...");

        //
        // ------------ load, update & save primary header ------------
        //
        if (!suppress_extension) {
            KMO_TRY_EXIT_IF_NULL(
                fn_suffix = cpl_sprintf("%s", suffix));
        } else {
            KMO_TRY_EXIT_IF_NULL(
                fn_suffix = cpl_sprintf("%s", ""));
        }

        // update which IFUs are not used
        KMO_TRY_EXIT_IF_ERROR(
            kmo_set_unused_ifus(unused_ifus_after, main_header,
                                "kmo_wave_cal"));

        KMO_TRY_EXIT_IF_NULL(
            frame = kmo_dfs_get_frame(frameset, ARC_ON));

        KMO_TRY_EXIT_IF_ERROR(
            kmo_dfs_save_main_header(frameset, filename_lcal, fn_suffix, frame,
                                     main_header, parlist, cpl_func));

        KMO_TRY_EXIT_IF_ERROR(
            kmo_dfs_save_main_header(frameset, filename_det_img, fn_suffix, frame,
                                     main_header, parlist, cpl_func));

        cpl_propertylist_delete(main_header); main_header = NULL;

        //
        // --- save sub-frames ---
        //
        for (a = 0; a < nr_angles; a++) {
            for (i = 1; i <= nr_devices; i++) {
                int sx = a * nr_devices + (i - 1);
                // save lcal-frame
                KMO_TRY_EXIT_IF_ERROR(
                    kmo_dfs_save_image(stored_lcal[sx], filename_lcal, fn_suffix,
                                       stored_sub_headers_lcal[sx], 0./0.));

                // save detector image
                KMO_TRY_EXIT_IF_ERROR(
                    kmo_dfs_save_image(stored_det_img[sx], filename_det_img, fn_suffix,
                                       stored_sub_headers_det_img[sx], 0./0.));
            } // for i = nr_devices
        } // for a angles

        // print which IFUs are not used
        kmo_print_unused_ifus(unused_ifus_after, TRUE);
    }
    KMO_CATCH
    {
        KMO_CATCH_MSG();
        ret_val = -1;
    }

    if (last_env != NULL) {
        setenv("KMCLIPM_PRIV_RECONSTRUCT_LUT_MODE",last_env,1);
    } else {
        unsetenv("KMCLIPM_PRIV_RECONSTRUCT_LUT_MODE");
    }

    kmo_free_fits_desc(&desc1);
    kmo_free_fits_desc(&desc2);
    if (unused_ifus_before != NULL) {
       kmo_free_unused_ifus(unused_ifus_before); unused_ifus_before = NULL;
    }
    if (unused_ifus_after != NULL) {
       kmo_free_unused_ifus(unused_ifus_after); unused_ifus_after = NULL;
    }
    cpl_propertylist_delete(main_header); main_header = NULL;
    cpl_image_delete(det_lamp_on); det_lamp_on = NULL;
    cpl_image_delete(det_lamp_off); det_lamp_off = NULL;
    cpl_image_delete(bad_pix_mask); bad_pix_mask = NULL;
    cpl_table_delete(arclines); arclines = NULL;
    cpl_table_delete(reflines); reflines = NULL;
    cpl_free(stored_qc_arc_sat); stored_qc_arc_sat = NULL;
    cpl_free(stored_qc_ar_eff); stored_qc_ar_eff = NULL;
    cpl_free(stored_qc_ne_eff); stored_qc_ne_eff = NULL;
    for (i = 0; i < nr_devices * nr_angles; i++) {
        cpl_image_delete(stored_lcal[i]); stored_lcal[i] = NULL;
        cpl_image_delete(stored_det_img[i]); stored_det_img[i] = NULL;
        cpl_propertylist_delete(stored_sub_headers_lcal[i]);
            stored_sub_headers_lcal[i] = NULL;
        cpl_propertylist_delete(stored_sub_headers_det_img[i]);
            stored_sub_headers_det_img[i] = NULL;
    }
    for (i = 0; i < nr_angles; i++) {
        cpl_frameset_delete(angle_frameset[i]); angle_frameset[i] = NULL;
    }
    if (filter_ids != NULL) {
        for (i = 0; i < nr_devices; i++) {
            cpl_free(filter_ids[i]); filter_ids[i] = NULL;

        }
        cpl_free(filter_ids); filter_ids = NULL;
    }
    if (edge_table != NULL) {
        for (i = 0; i < nr_devices; i++) {
            cpl_free(edge_table[i]); edge_table[i] = NULL;
        }
        cpl_free(edge_table); edge_table = NULL;
    }

    cpl_free(angle_frameset); angle_frameset = NULL;
    cpl_free(stored_lcal); stored_lcal = NULL;
    cpl_free(stored_det_img); stored_det_img = NULL;
    cpl_free(stored_sub_headers_lcal); stored_sub_headers_lcal = NULL;
    cpl_free(stored_sub_headers_det_img); stored_sub_headers_det_img = NULL;
    cpl_free(readmode); readmode = NULL;
    cpl_bivector_delete(lines); lines = NULL;
    cpl_free(suffix); suffix = NULL;
    cpl_free(fn_suffix); fn_suffix = NULL;

    return ret_val;
}

/**@}*/
