/* $Id: fors_wave_calib_lss.c,v 1.10 2013-09-09 12:25:33 cgarcia Exp $
 *
 * This file is part of the FORS Data Reduction Pipeline
 * Copyright (C) 2002-2010 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 St, Fifth Floor, Boston, MA  02110-1301  USA
 */

/*
 * $Author: cgarcia $
 * $Date: 2013-09-09 12:25:33 $
 * $Revision: 1.10 $
 * $Name: not supported by cvs2svn $
 */

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

#include <math.h>
#include <cpl.h>
#include <moses.h>
#include <fors_dfs.h>

static int fors_wave_calib_lss_create(cpl_plugin *);
static int fors_wave_calib_lss_exec(cpl_plugin *);
static int fors_wave_calib_lss_destroy(cpl_plugin *);
static int fors_wave_calib_lss(cpl_parameterlist *, cpl_frameset *);

static char fors_wave_calib_lss_description[] =
"This recipe is used to wavelength calibrate one long slit spectrum, i.e.,\n"
"a FORS spectral obtained either in LSS mode or in MOS/MXU mode with all\n"
"slits at the same offset. A pattern-matching algorithm is applied as in\n"
"recipe fors_detect_spectra. For more details on this data reduction\n"
"strategy please refer to the FORS Pipeline User's Manual.\n"
"\n"
"Note that specifying an input GRISM_TABLE will set some of the recipe\n"
"configuration parameters to default values valid for a particular grism.\n"
"\n"
"In the table below the LSS acronym can be alternatively read as MOS or\n"
"MXU.\n\n"
"Input files:\n\n"
"  DO category:               Type:       Explanation:         Required:\n"
"  LAMP_UNBIAS_LSS            Calib       Arc lamp exposure       Y\n"
"  MASTER_LINECAT             Calib       Line catalog            Y\n"
"  GRISM_TABLE                Calib       Grism table             .\n\n"
"Output files:\n\n"
"  DO category:               Data type:  Explanation:\n"
"  REDUCED_LAMP_LSS           FITS image  Calibrated arc lamp exposure\n"
"  DISP_COEFF_LSS             FITS table  Inverse dispersion coefficients\n"
"  DISP_RESIDUALS_LSS         FITS image  Image of modeling residuals\n"
"  WAVELENGTH_MAP_LSS         FITS image  Wavelengths mapped on CCD\n"
"  SLIT_LOCATION_LSS          FITS image  Background subtracted arc frame\n"
"  SPECTRAL_RESOLUTION_LSS    FITS table  Spectral resolution table\n\n";

#define fors_wave_calib_lss_exit(message)     \
{                                             \
if ((const char *)message != NULL) cpl_msg_error(recipe, message);  \
cpl_image_delete(spectra);                    \
cpl_image_delete(residual);                   \
cpl_image_delete(rectified);                  \
cpl_image_delete(wavemap);                    \
cpl_table_delete(grism_table);                \
cpl_table_delete(wavelengths);                \
cpl_table_delete(maskslits);                  \
cpl_table_delete(idscoeff);                   \
cpl_table_delete(idscoeff_all);               \
cpl_table_delete(restab);                     \
cpl_table_delete(slits);                      \
cpl_vector_delete(lines);                     \
cpl_propertylist_delete(header);              \
cpl_propertylist_delete(save_header);         \
cpl_msg_indent_less();                        \
return -1;                                    \
}

#define fors_wave_calib_lss_exit_memcheck(message)  \
{                                                   \
if ((const char *)message != NULL) cpl_msg_info(recipe, message);         \
printf("free spectra (%p)\n", spectra);             \
cpl_image_delete(spectra);                          \
printf("free residual (%p)\n", residual);           \
cpl_image_delete(residual);                         \
printf("free rectified (%p)\n", rectified);         \
cpl_image_delete(rectified);                        \
printf("free wavemap (%p)\n", wavemap);             \
cpl_image_delete(wavemap);                          \
printf("free grism_table (%p)\n", grism_table);     \
cpl_table_delete(grism_table);                      \
printf("free wavelengths (%p)\n", wavelengths);     \
cpl_table_delete(wavelengths);                      \
printf("free maskslits (%p)\n", maskslits);         \
cpl_table_delete(maskslits);                        \
printf("free idscoeff (%p)\n", idscoeff);           \
cpl_table_delete(idscoeff);                         \
printf("free idscoeff_all (%p)\n", idscoeff_all);   \
cpl_table_delete(idscoeff_all);                     \
printf("free restab (%p)\n", restab);               \
cpl_table_delete(restab);                           \
printf("free slits (%p)\n", slits);                 \
cpl_table_delete(slits);                            \
printf("free lines (%p)\n", lines);                 \
cpl_vector_delete(lines);                           \
printf("free header (%p)\n", header);               \
cpl_propertylist_delete(header);                    \
printf("free save_header (%p)\n", save_header);     \
cpl_propertylist_delete(save_header);               \
cpl_msg_indent_less();                              \
return 0;                                           \
}


/**
 * @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,
                    FORS_BINARY_VERSION,
                    CPL_PLUGIN_TYPE_RECIPE,
                    "fors_wave_calib_lss",
                    "Derive dispersion relation from long-slit arc lamp frame",
                    fors_wave_calib_lss_description,
                    "Carlo Izzo",
                    PACKAGE_BUGREPORT,
    "This file is currently part of the FORS Instrument Pipeline\n"
    "Copyright (C) 2002-2010 European Southern Observatory\n\n"
    "This program is free software; you can redistribute it and/or modify\n"
    "it under the terms of the GNU General Public License as published by\n"
    "the Free Software Foundation; either version 2 of the License, or\n"
    "(at your option) any later version.\n\n"
    "This program is distributed in the hope that it will be useful,\n"
    "but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
    "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n"
    "GNU General Public License for more details.\n\n"
    "You should have received a copy of the GNU General Public License\n"
    "along with this program; if not, write to the Free Software Foundation,\n"
    "Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA\n",
                    fors_wave_calib_lss_create,
                    fors_wave_calib_lss_exec,
                    fors_wave_calib_lss_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 fors_wave_calib_lss_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 (empty) parameters list in the cpl_recipe object 
     */

    recipe->parameters = cpl_parameterlist_new(); 

    /*
     * Dispersion
     */

    p = cpl_parameter_new_value("fors.fors_wave_calib_lss.dispersion",
                                CPL_TYPE_DOUBLE,
                                "Expected spectral dispersion (Angstrom/pixel)",
                                "fors.fors_wave_calib_lss",
                                0.0);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "dispersion");
    cpl_parameter_disable(p, CPL_PARAMETER_MODE_ENV);
    cpl_parameterlist_append(recipe->parameters, p);

    /*
     * Peak detection level
     */

    p = cpl_parameter_new_value("fors.fors_wave_calib_lss.peakdetection",
                                CPL_TYPE_DOUBLE,
                                "Initial peak detection threshold (ADU)",
                                "fors.fors_wave_calib_lss",
                                0.0);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "peakdetection");
    cpl_parameter_disable(p, CPL_PARAMETER_MODE_ENV);
    cpl_parameterlist_append(recipe->parameters, p);

    /*
     * Degree of wavelength calibration polynomial
     */

    p = cpl_parameter_new_value("fors.fors_wave_calib_lss.wdegree",
                                CPL_TYPE_INT,
                                "Degree of wavelength calibration polynomial",
                                "fors.fors_wave_calib_lss",
                                0);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "wdegree");
    cpl_parameter_disable(p, CPL_PARAMETER_MODE_ENV);
    cpl_parameterlist_append(recipe->parameters, p);

    /*
     * Reference lines search radius
     */

    p = cpl_parameter_new_value("fors.fors_wave_calib_lss.wradius",
                                CPL_TYPE_INT,
                                "Search radius if iterating pattern-matching "
                                "with first-guess method",
                                "fors.fors_wave_calib_lss",
                                4);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "wradius");
    cpl_parameter_disable(p, CPL_PARAMETER_MODE_ENV);
    cpl_parameterlist_append(recipe->parameters, p);

    /*
     * Rejection threshold in dispersion relation polynomial fitting
     */

    p = cpl_parameter_new_value("fors.fors_wave_calib_lss.wreject",
                                CPL_TYPE_DOUBLE,
                                "Rejection threshold in dispersion "
                                "relation fit (pixel)",
                                "fors.fors_wave_calib_lss",
                                0.7);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "wreject");
    cpl_parameter_disable(p, CPL_PARAMETER_MODE_ENV);
    cpl_parameterlist_append(recipe->parameters, p);

    /*
     * Line catalog table column containing the reference wavelengths
     */

    p = cpl_parameter_new_value("fors.fors_wave_calib_lss.wcolumn",
                                CPL_TYPE_STRING,
                                "Name of line catalog table column "
                                "with wavelengths",
                                "fors.fors_wave_calib_lss",
                                "WLEN");
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "wcolumn");
    cpl_parameter_disable(p, CPL_PARAMETER_MODE_ENV);
    cpl_parameterlist_append(recipe->parameters, p);

    /*
     * Start wavelength for spectral extraction
     */

    p = cpl_parameter_new_value("fors.fors_wave_calib_lss.startwavelength",
                                CPL_TYPE_DOUBLE,
                                "Start wavelength in spectral extraction",
                                "fors.fors_wave_calib_lss",
                                0.0);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "startwavelength");
    cpl_parameter_disable(p, CPL_PARAMETER_MODE_ENV);
    cpl_parameterlist_append(recipe->parameters, p);

    /*
     * End wavelength for spectral extraction
     */

    p = cpl_parameter_new_value("fors.fors_wave_calib_lss.endwavelength",
                                CPL_TYPE_DOUBLE,
                                "End wavelength in spectral extraction",
                                "fors.fors_wave_calib_lss",
                                0.0);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "endwavelength");
    cpl_parameter_disable(p, CPL_PARAMETER_MODE_ENV);
    cpl_parameterlist_append(recipe->parameters, p);

    /*
     * Wavelength solution interpolation
     */
 
    p = cpl_parameter_new_value("fors.fors_wave_calib_lss.wmode",
                                CPL_TYPE_INT,
                                "Interpolation mode of wavelength solution "
                                "(0 = no interpolation, 1 = fill gaps, "
                                "2 = global model)",
                                "fors.fors_wave_calib_lss",
                                2);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "wmode");
    cpl_parameter_disable(p, CPL_PARAMETER_MODE_ENV);
    cpl_parameterlist_append(recipe->parameters, p);

    return 0;
}


/**
 * @brief    Execute the plugin instance given by the interface
 *
 * @param    plugin  the plugin
 *
 * @return   0 if everything is ok
 */

static int fors_wave_calib_lss_exec(cpl_plugin *plugin)
{
    cpl_recipe *recipe;
    
    if (cpl_plugin_get_type(plugin) == CPL_PLUGIN_TYPE_RECIPE) 
        recipe = (cpl_recipe *)plugin;
    else 
        return -1;

    return fors_wave_calib_lss(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 fors_wave_calib_lss_destroy(cpl_plugin *plugin)
{
    cpl_recipe *recipe;
    
    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 set-of-frames
 *
 * @return   0 if everything is ok
 */

static int fors_wave_calib_lss(cpl_parameterlist *parlist, 
                               cpl_frameset *frameset)
{

    const char *recipe = "fors_wave_calib_lss";


    /*
     * Input parameters
     */

    double      dispersion;
    double      peakdetection;
    int         wdegree;
    int         wradius;
    double      wreject;
    int         wmode;
    const char *wcolumn;
    double      startwavelength;
    double      endwavelength;

    /*
     * CPL objects
     */

    cpl_image        *spectra      = NULL;
    cpl_image        *rectified    = NULL;
    cpl_image        *wavemap      = NULL;
    cpl_image        *residual     = NULL;
    cpl_image        *dummy        = NULL;
    cpl_table        *grism_table  = NULL;
    cpl_table        *wavelengths  = NULL;
    cpl_table        *slits        = NULL;
    cpl_table        *idscoeff     = NULL;
    cpl_table        *idscoeff_all = NULL;
    cpl_table        *maskslits    = NULL;
    cpl_table        *restab       = NULL;
    cpl_vector       *lines        = NULL;
    cpl_propertylist *header       = NULL;
    cpl_propertylist *save_header  = NULL;

    /*
     * Auxiliary variables
     */

    char        version[80];
    const char *arc_tag;
    const char *reduced_lamp_tag;
    const char *wavelength_map_tag;
    const char *disp_residuals_tag;
    const char *disp_coeff_tag;
    const char *slit_location_tag;
    const char *spectral_resolution_tag;
    int         mxu;
    int         mos;
    int         lss;
    int         treat_as_lss = 0;
    double      mean_rms;
    int         narc;
    int         nlines;
    int         rebin;
    double     *line;
    double     *fiterror = NULL;
    int        *fitlines = NULL;
    int         nx, ny;
    int         first_row, last_row;
    int         ylow, yhig;
    double      reference;
    int         i;

    char       *instrume = NULL;


    cpl_msg_set_indentation(2);

    /*
     * Get configuration parameters
     */

    cpl_msg_info(recipe, "Recipe %s configuration parameters:", recipe);
    cpl_msg_indent_more();
    
    if (cpl_frameset_count_tags(frameset, "GRISM_TABLE") > 1)
        fors_wave_calib_lss_exit("Too many in input: GRISM_TABLE"); 

    grism_table = dfs_load_table(frameset, "GRISM_TABLE", 1);

    dispersion = dfs_get_parameter_double(parlist,
                    "fors.fors_wave_calib_lss.dispersion", grism_table);

    if (dispersion <= 0.0)
        fors_wave_calib_lss_exit("Invalid spectral dispersion value");

    peakdetection = dfs_get_parameter_double(parlist,
                    "fors.fors_wave_calib_lss.peakdetection", grism_table);
    if (peakdetection <= 0.0)
        fors_wave_calib_lss_exit("Invalid peak detection level");

    wdegree = dfs_get_parameter_int(parlist,
                    "fors.fors_wave_calib_lss.wdegree", grism_table);

    if (wdegree < 1)
        fors_wave_calib_lss_exit("Invalid polynomial degree");

    if (wdegree > 5)
        fors_wave_calib_lss_exit("Max allowed polynomial degree is 5");

    wradius = dfs_get_parameter_int(parlist, 
                                    "fors.fors_wave_calib_lss.wradius", NULL);

    if (wradius < 0)
        fors_wave_calib_lss_exit("Invalid search radius");

    wreject = dfs_get_parameter_double(parlist, 
                                    "fors.fors_wave_calib_lss.wreject", NULL);

    if (wreject <= 0.0)
        fors_wave_calib_lss_exit("Invalid rejection threshold");

    wmode = dfs_get_parameter_int(parlist, 
                                    "fors.fors_wave_calib_lss.wmode", NULL);

    if (wmode < 0 || wmode > 2)
        fors_wave_calib_lss_exit("Invalid interpolation mode");

    wcolumn = dfs_get_parameter_string(parlist, 
                                    "fors.fors_wave_calib_lss.wcolumn", NULL);

    startwavelength = dfs_get_parameter_double(parlist,
                    "fors.fors_wave_calib_lss.startwavelength", grism_table);
    if (startwavelength > 1.0)
        if (startwavelength < 3000.0 || startwavelength > 13000.0)
            fors_wave_calib_lss_exit("Invalid wavelength");

    endwavelength = dfs_get_parameter_double(parlist,
                    "fors.fors_wave_calib_lss.endwavelength", grism_table);
    if (endwavelength > 1.0) {
        if (endwavelength < 3000.0 || endwavelength > 13000.0)
            fors_wave_calib_lss_exit("Invalid wavelength");
        if (startwavelength < 1.0)
            fors_wave_calib_lss_exit("Invalid wavelength interval");
    }

    if (startwavelength > 1.0)
        if (endwavelength - startwavelength <= 0.0)
            fors_wave_calib_lss_exit("Invalid wavelength interval");

    cpl_table_delete(grism_table); grism_table = NULL;

    if (cpl_error_get_code())
        fors_wave_calib_lss_exit("Failure reading the configuration "
                                 "parameters");


    cpl_msg_indent_less();
    cpl_msg_info(recipe, "Check input set-of-frames:");
    cpl_msg_indent_more();

    if (cpl_frameset_count_tags(frameset, "MASTER_LINECAT") == 0)
        fors_wave_calib_lss_exit("Missing required input: MASTER_LINECAT");

    if (cpl_frameset_count_tags(frameset, "MASTER_LINECAT") > 1)
        fors_wave_calib_lss_exit("Too many in input: MASTER_LINECAT");

    mxu = cpl_frameset_count_tags(frameset, "LAMP_UNBIAS_MXU");
    mos = cpl_frameset_count_tags(frameset, "LAMP_UNBIAS_MOS");
    lss = cpl_frameset_count_tags(frameset, "LAMP_UNBIAS_LSS");

    narc = mxu + mos + lss;

    if (narc == 0) {
        fors_wave_calib_lss_exit("Missing input long-slit arc lamp frame");
    }
    if (narc > 1) {
        cpl_msg_error(recipe, "Too many input arc lamp frames (%d > 1)", narc);
        fors_wave_calib_lss_exit(NULL);
    }

    if (mxu) {
        arc_tag                 = "LAMP_UNBIAS_MXU";
        slit_location_tag       = "SLIT_LOCATION_MXU";
        reduced_lamp_tag        = "REDUCED_LAMP_MXU";
        disp_residuals_tag      = "DISP_RESIDUALS_MXU";
        disp_coeff_tag          = "DISP_COEFF_MXU";
        wavelength_map_tag      = "WAVELENGTH_MAP_MXU";
        spectral_resolution_tag = "SPECTRAL_RESOLUTION_MXU";
    }
    else if (mos) {
        arc_tag                 = "LAMP_UNBIAS_MOS";
        slit_location_tag       = "SLIT_LOCATION_MOS";
        reduced_lamp_tag        = "REDUCED_LAMP_MOS";
        disp_residuals_tag      = "DISP_RESIDUALS_MOS";
        disp_coeff_tag          = "DISP_COEFF_MOS";
        wavelength_map_tag      = "WAVELENGTH_MAP_MOS";
        spectral_resolution_tag = "SPECTRAL_RESOLUTION_MOS";
    }
    else if (lss) {
        arc_tag                 = "LAMP_UNBIAS_LSS";
        slit_location_tag       = "SLIT_LOCATION_LSS";
        reduced_lamp_tag        = "REDUCED_LAMP_LSS";
        disp_residuals_tag      = "DISP_RESIDUALS_LSS";
        disp_coeff_tag          = "DISP_COEFF_LSS";
        wavelength_map_tag      = "WAVELENGTH_MAP_LSS";
        spectral_resolution_tag = "SPECTRAL_RESOLUTION_LSS";
    }


    if (!dfs_equal_keyword(frameset, "ESO INS GRIS1 ID"))
        cpl_msg_warning(cpl_func,"Input frames are not from the same grism");

    if (!dfs_equal_keyword(frameset, "ESO INS FILT1 ID"))
        cpl_msg_warning(cpl_func,"Input frames are not from the same filter");

    if (!dfs_equal_keyword(frameset, "ESO DET CHIP1 ID"))
        cpl_msg_warning(cpl_func,"Input frames are not from the same chip");


    /*
     * Get the reference wavelength and the rebin factor along the
     * dispersion direction from the arc lamp exposure
     */

    header = dfs_load_header(frameset, arc_tag, 0);

    if (header == NULL)
        fors_wave_calib_lss_exit("Cannot load arc lamp header");

    instrume = (char *)cpl_propertylist_get_string(header, "INSTRUME");
    if (instrume == NULL)
        fors_wave_calib_lss_exit("Missing keyword INSTRUME in arc lamp header");

    if (instrume[4] == '1')
        snprintf(version, 80, "%s/%s", "fors1", VERSION);
    if (instrume[4] == '2')
        snprintf(version, 80, "%s/%s", "fors2", VERSION);

    reference = cpl_propertylist_get_double(header, "ESO INS GRIS1 WLEN");

    if (cpl_error_get_code() != CPL_ERROR_NONE)
        fors_wave_calib_lss_exit("Missing keyword ESO INS GRIS1 WLEN "
                                 "in arc lamp frame header");

    if (reference < 3000.0)   /* Perhaps in nanometers... */
        reference *= 10;

    if (reference < 3000.0 || reference > 13000.0) {
        cpl_msg_error(recipe, "Invalid central wavelength %.2f read from "
                      "keyword ESO INS GRIS1 WLEN in arc lamp frame header",
                      reference);
        fors_wave_calib_lss_exit(NULL);
    }

    cpl_msg_info(recipe, "The central wavelength is: %.2f", reference);

    rebin = cpl_propertylist_get_int(header, "ESO DET WIN1 BINX");

    if (cpl_error_get_code() != CPL_ERROR_NONE)
        fors_wave_calib_lss_exit("Missing keyword ESO DET WIN1 BINX "
                                 "in arc lamp frame header");

    if (rebin != 1) {
        dispersion *= rebin;
        cpl_msg_warning(recipe, "The rebin factor is %d, and therefore the "
                        "working dispersion used is %f A/pixel", rebin,
                        dispersion);
    }


    if (mos || mxu) {

        int nslits_out_det = 0;

        /*
         * Check if all slits have the same X offset. If not, this is the
         * wrong recipe...
         */

        if (mos)
            maskslits = mos_load_slits_fors_mos(header, &nslits_out_det);
        else
            maskslits = mos_load_slits_fors_mxu(header);

        treat_as_lss = fors_mos_is_lss_like(maskslits, nslits_out_det);

        cpl_table_delete(maskslits); maskslits = NULL;

        if (!treat_as_lss)
            fors_wave_calib_lss_exit("Input data are not long-slit data");
    }


    cpl_msg_indent_less();
    cpl_msg_info(recipe, "Load arc lamp exposure...");
    cpl_msg_indent_more();

    spectra = dfs_load_image(frameset, arc_tag, CPL_TYPE_FLOAT, 0, 0);

    if (spectra == NULL)
        fors_wave_calib_lss_exit("Cannot load arc lamp exposure");


    cpl_msg_indent_less();
    cpl_msg_info(recipe, "Load input line catalog...");
    cpl_msg_indent_more();

    wavelengths = dfs_load_table(frameset, "MASTER_LINECAT", 1);

    if (wavelengths == NULL)
        fors_wave_calib_lss_exit("Cannot load line catalog");


    /*
     * Cast the wavelengths into a (double precision) CPL vector
     */

    nlines = cpl_table_get_nrow(wavelengths);

    if (nlines == 0)
        fors_wave_calib_lss_exit("Empty input line catalog");

    if (cpl_table_has_column(wavelengths, wcolumn) != 1) {
        cpl_msg_error(recipe, "Missing column %s in input line catalog table",
                      wcolumn);
        fors_wave_calib_lss_exit(NULL);
    }

    line = cpl_malloc(nlines * sizeof(double));

    for (i = 0; i < nlines; i++)
        line[i] = cpl_table_get(wavelengths, wcolumn, i, NULL);

    cpl_table_delete(wavelengths); wavelengths = NULL;

    lines = cpl_vector_wrap(nlines, line);


    cpl_msg_indent_less();
    cpl_msg_info(recipe, "Perform wavelength calibration...");
    cpl_msg_indent_more();

    nx = cpl_image_get_size_x(spectra);
    ny = cpl_image_get_size_y(spectra);

    wavemap = cpl_image_new(nx, ny, CPL_TYPE_FLOAT);
    idscoeff_all = cpl_table_new(ny);

    if (mos_saturation_process(spectra))
	fors_wave_calib_lss_exit("Cannot process saturation");

    if (mos_subtract_background(spectra))
	fors_wave_calib_lss_exit("Cannot subtract the background");

    rectified = mos_wavelength_calibration_raw(spectra, lines, dispersion,
                                               peakdetection, wradius,
                                               wdegree, wreject, reference,
                                               &startwavelength,
                                               &endwavelength, NULL,
                                               NULL, idscoeff_all, wavemap,
                                               NULL, NULL, NULL, NULL);

    if (rectified == NULL)
        fors_wave_calib_lss_exit("Wavelength calibration failure.");

    cpl_image_delete(rectified); rectified = NULL;

    first_row = 0;
    while (!cpl_table_is_valid(idscoeff_all, "c0", first_row))
        first_row++;

    last_row = ny - 1;
    while (!cpl_table_is_valid(idscoeff_all, "c0", last_row))
        last_row--;

    ylow = first_row + 1;
    yhig = last_row + 1;

    dummy = cpl_image_extract(spectra, 1, ylow, nx, yhig);
    cpl_image_delete(spectra); spectra = dummy;

    ny = cpl_image_get_size_y(spectra);

    residual = cpl_image_new(nx, ny, CPL_TYPE_FLOAT);

    fiterror = cpl_calloc(ny, sizeof(double));
    fitlines = cpl_calloc(ny, sizeof(int));
    idscoeff = cpl_table_new(ny);

    if (mos_saturation_process(spectra))
	fors_wave_calib_lss_exit("Cannot process saturation");

    if (mos_subtract_background(spectra))
	fors_wave_calib_lss_exit("Cannot subtract the background");

    rectified = mos_wavelength_calibration_raw(spectra, lines, dispersion,
                                               peakdetection, wradius,
                                               wdegree, wreject, reference,
                                               &startwavelength,
                                               &endwavelength, fitlines,
                                               fiterror, idscoeff, NULL,
                                               residual, NULL, NULL, NULL);

    if (rectified == NULL)
        fors_wave_calib_lss_exit("Wavelength calibration failure.");

    /*
     * A dummy slit locations table
     */

    slits = cpl_table_new(1);
    cpl_table_new_column(slits, "slit_id", CPL_TYPE_INT);
    cpl_table_new_column(slits, "xtop", CPL_TYPE_DOUBLE);
    cpl_table_new_column(slits, "ytop", CPL_TYPE_DOUBLE);
    cpl_table_new_column(slits, "xbottom", CPL_TYPE_DOUBLE);
    cpl_table_new_column(slits, "ybottom", CPL_TYPE_DOUBLE);
    cpl_table_new_column(slits, "position", CPL_TYPE_INT);
    cpl_table_new_column(slits, "length", CPL_TYPE_INT);
    cpl_table_set_column_unit(slits, "xtop", "pixel");
    cpl_table_set_column_unit(slits, "ytop", "pixel");
    cpl_table_set_column_unit(slits, "xbottom", "pixel");
    cpl_table_set_column_unit(slits, "ybottom", "pixel");
    cpl_table_set_column_unit(slits, "position", "pixel");
    cpl_table_set_column_unit(slits, "length", "pixel");
    cpl_table_set_int(slits, "slit_id", 0, 0);
    cpl_table_set_double(slits, "xtop", 0, 0);
    cpl_table_set_double(slits, "ytop", 0, last_row);
    cpl_table_set_double(slits, "xbottom", 0, 0);
    cpl_table_set_double(slits, "ybottom", 0, first_row);
    cpl_table_set_int(slits, "position", 0, 0);
    cpl_table_set_int(slits, "length", 0, ny);

    if (dfs_save_table(frameset, slits, slit_location_tag, NULL,
                       parlist, recipe, version))
        fors_wave_calib_lss_exit(NULL);

    cpl_table_delete(slits); slits = NULL;

    if (wmode) {
        cpl_image_delete(rectified); rectified = NULL;
        cpl_image_delete(wavemap); wavemap = NULL;

        /*
         * Wavemap is intentionally NULL in the next two calls
         */

        mos_interpolate_wavecalib(idscoeff, wavemap, wmode, 2);
        mos_interpolate_wavecalib(idscoeff_all, wavemap, wmode, 2);

        wavemap = mos_map_idscoeff(idscoeff_all, nx, reference,
                                   startwavelength, endwavelength);
        rectified = mos_wavelength_calibration(spectra, reference,
                                               startwavelength,
                                               endwavelength, dispersion,
                                               idscoeff, 0);
    }

    cpl_table_delete(idscoeff_all); idscoeff_all = NULL;

    cpl_table_wrap_double(idscoeff, fiterror, "error"); fiterror = NULL;
    cpl_table_set_column_unit(idscoeff, "error", "pixel");
    cpl_table_wrap_int(idscoeff, fitlines, "nlines"); fitlines = NULL;

    for (i = 0; i < ny; i++)
        if (!cpl_table_is_valid(idscoeff, "c0", i))
            cpl_table_set_invalid(idscoeff, "error", i);

    cpl_msg_info(recipe, "Valid solutions found: %d out of %d rows",
                 ny - cpl_table_count_invalid(idscoeff, "c0"), ny);

    cpl_image_delete(spectra); spectra = NULL;

    mean_rms = mos_distortions_rms(rectified, lines, startwavelength,
                                   dispersion, 6, 0);

    cpl_msg_info(recipe, "Mean residual: %f pixel", mean_rms);

    mean_rms = cpl_table_get_column_mean(idscoeff, "error");

    cpl_msg_info(recipe, "Mean model accuracy: %f pixel (%f A)",
                 mean_rms, mean_rms * dispersion);

    restab = mos_resolution_table(rectified, startwavelength, dispersion,
                                  60000, lines);

    if (restab) {
        cpl_msg_info(recipe, "Mean spectral resolution: %.2f",
              cpl_table_get_column_mean(restab, "resolution"));
        cpl_msg_info(recipe,
              "Mean reference lines FWHM: %.2f +/- %.2f pixel",
              cpl_table_get_column_mean(restab, "fwhm") / dispersion,
              cpl_table_get_column_mean(restab, "fwhm_rms") / dispersion);

        if (dfs_save_table(frameset, restab, spectral_resolution_tag,
                           NULL, parlist, recipe, version))
            fors_wave_calib_lss_exit(NULL);

        cpl_table_delete(restab); restab = NULL;

    }
    else
        fors_wave_calib_lss_exit("Cannot compute the spectral "
                                 "resolution table");

    cpl_vector_delete(lines); lines = NULL;


    /*
     * Save rectified arc lamp spectrum to disk
     */

    save_header = cpl_propertylist_new();
    cpl_propertylist_update_double(save_header, "CRPIX1", 1.0);
    cpl_propertylist_update_double(save_header, "CRPIX2", 1.0);
    cpl_propertylist_update_double(save_header, "CRVAL1",
                                   startwavelength + dispersion/2);
    cpl_propertylist_update_double(save_header, "CRVAL2", 1.0);
    /* cpl_propertylist_update_double(save_header, "CDELT1", dispersion);
    cpl_propertylist_update_double(save_header, "CDELT2", 1.0); */
    cpl_propertylist_update_double(save_header, "CD1_1", dispersion);
    cpl_propertylist_update_double(save_header, "CD1_2", 0.0);
    cpl_propertylist_update_double(save_header, "CD2_1", 0.0);
    cpl_propertylist_update_double(save_header, "CD2_2", 1.0);
    cpl_propertylist_update_string(save_header, "CTYPE1", "LINEAR");
    cpl_propertylist_update_string(save_header, "CTYPE2", "PIXEL");
    cpl_propertylist_update_int(save_header, "ESO PRO DATANCOM", 1);

    if (dfs_save_image(frameset, rectified, reduced_lamp_tag, save_header,
                       parlist, recipe, version))
        fors_wave_calib_lss_exit(NULL);

    cpl_image_delete(rectified); rectified = NULL;
    cpl_propertylist_delete(save_header); save_header = NULL;

    if (dfs_save_table(frameset, idscoeff, disp_coeff_tag, NULL,
                       parlist, recipe, version))
        fors_wave_calib_lss_exit(NULL);

    cpl_table_delete(idscoeff); idscoeff = NULL;

    if (dfs_save_image(frameset, wavemap, wavelength_map_tag, header,
                       parlist, recipe, version))
        fors_wave_calib_lss_exit(NULL);

    cpl_image_delete(wavemap); wavemap = NULL;
    cpl_propertylist_delete(header); header = NULL;
    header = cpl_propertylist_new();

    cpl_propertylist_update_double(header, "CRPIX2", 1.0);
    cpl_propertylist_update_double(header, "CRVAL2", 1.0);
    /* cpl_propertylist_update_double(header, "CDELT2", 1.0); */
    cpl_propertylist_update_double(header, "CD1_1", 1.0);
    cpl_propertylist_update_double(header, "CD1_2", 0.0);
    cpl_propertylist_update_double(header, "CD2_1", 0.0);
    cpl_propertylist_update_double(header, "CD2_2", 1.0);
    cpl_propertylist_update_string(header, "CTYPE1", "LINEAR");
    cpl_propertylist_update_string(header, "CTYPE2", "PIXEL");

    if (dfs_save_image(frameset, residual, disp_residuals_tag, header,
                       parlist, recipe, version))
        fors_wave_calib_lss_exit(NULL);

    cpl_image_delete(residual); residual = NULL;
    cpl_propertylist_delete(header); header = NULL;

    return 0;
}
