/*
 * This file is part of the GIRAFFE Pipeline
 * Copyright (C) 2002-2019 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
 */

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

#include <assert.h>

#include <cxslist.h>
#include <cxmessages.h>

#include <cpl_recipe.h>
#include <cpl_plugininfo.h>
#include <cpl_parameterlist.h>
#include <cpl_frameset.h>
#include <cpl_msg.h>
#include <cpl_errorstate.h>

#include <irplib_sdp_spectrum.h>

#include "gialias.h"
#include "giframe.h"
#include "gifibers.h"
#include "gifiberutils.h"
#include "gislitgeometry.h"
#include "gipsfdata.h"
#include "gibias.h"
#include "gidark.h"
#include "giextract.h"
#include "giflat.h"
#include "gitransmission.h"
#include "girebinning.h"
#include "gisgcalibration.h"
#include "giastrometry.h"
#include "gifov.h"
#include "gimessages.h"
#include "gierror.h"
#include "giutils.h"


static cxint giscience(cpl_parameterlist*, cpl_frameset*);

static cxint _giraffe_make_sdp_spectra(const cxchar* flux_filename,
                                       const cxchar* err_filename,
                                       cxint nassoc_keys,
                                       cpl_frameset* allframes,
                                       const cpl_parameterlist* parlist,
                                       const cxchar* recipe_id);


/*
 * Create the recipe instance, i.e. setup the parameter list for this
 * recipe and make it available to the application using the interface.
 */

static cxint
giscience_create(cpl_plugin* plugin)
{

    cpl_recipe* recipe = (cpl_recipe*)plugin;

    cpl_parameter* p = NULL;


    giraffe_error_init();


    /*
     * We have to provide the option we accept to the application. We
     * need to setup our parameter list and hook it into the recipe
     * interface.
     */

    recipe->parameters = cpl_parameterlist_new();
    cx_assert(recipe->parameters != NULL);


    /*
     * Fill the parameter list.
     */

    /* Bias removal */

    giraffe_bias_config_add(recipe->parameters);

    /* Dark subtraction */

    /* TBD */

    /* Spectrum extraction */

    giraffe_extract_config_add(recipe->parameters);

    /* Flat fielding and relative fiber transmission correction */

    giraffe_flat_config_add(recipe->parameters);

    /* Spectrum rebinning */

    giraffe_rebin_config_add(recipe->parameters);

    /* Simultaneous wavelength calibration correction */

    p = cpl_parameter_new_value("giraffe.siwc.apply",
                                CPL_TYPE_BOOL,
                                "Enable simultaneous wavelength calibration "
                                "correction.",
                                "giraffe.siwc",
                                TRUE);

    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "siwc-apply");
    cpl_parameterlist_append(recipe->parameters, p);

    giraffe_sgcalibration_config_add(recipe->parameters);

    /* Image reconstruction (IFU and Argus only) */

    giraffe_fov_config_add(recipe->parameters);

    /* Science Data Product format generation parameters: */

    p = cpl_parameter_new_value("giraffe.sdp.format.generate",
                                CPL_TYPE_BOOL,
                                "TRUE if additional files should be generated"
                                " in Science Data Product (SDP) format.",
                                "giraffe.sdp",
                                FALSE);

    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "generate-SDP-format");
    cpl_parameterlist_append(recipe->parameters, p);

    p = cpl_parameter_new_value("giraffe.sdp.nassoc.keys",
                                CPL_TYPE_INT,
                                "Sets the number of dummy (empty) ASSONi,"
                                " ASSOCi and ASSOMi keywords to create.",
                                "giraffe.sdp",
                                (int)0);

    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI,
                            "dummy-association-keys");
    cpl_parameterlist_append(recipe->parameters, p);

    return 0;

}


/*
 * Execute the plugin instance given by the interface.
 */

static cxint
giscience_exec(cpl_plugin* plugin)
{
    cxint result;
    cpl_errorstate prev_state;

    cpl_recipe* recipe = (cpl_recipe*)plugin;


    cx_assert(recipe->parameters != NULL);
    cx_assert(recipe->frames != NULL);

    prev_state = cpl_errorstate_get();
    result = giscience(recipe->parameters, recipe->frames);
    if (result != 0) {
        cpl_errorstate_dump(prev_state, CPL_FALSE, cpl_errorstate_dump_one);
    }
    return result;
}


static cxint
giscience_destroy(cpl_plugin* plugin)
{

    cpl_recipe* recipe = (cpl_recipe*)plugin;


    /*
     * We just destroy what was created during the plugin initialization
     * phase, i.e. the parameter list. The frame set is managed by the
     * application which called us, so we must not touch it,
     */

    cpl_parameterlist_delete(recipe->parameters);

    giraffe_error_clear();

    return 0;

}


/*
 * The actual recipe starts here.
 */

static cxint
giscience(cpl_parameterlist* config, cpl_frameset* set)
{

    const cxchar* const _id = "giscience";


    const cxchar* filename = NULL;

    cxbool siwc = FALSE;
    cxbool calsim = FALSE;

    cxbool gensdp = FALSE;
    cxint nassoc_keys = 0;
    cxchar* flux_filename = NULL;
    cxchar* err_filename = NULL;

    cxint status = 0;

    cxlong i;
    cxlong nscience = 0;

    cxdouble exptime = 0.;

    cx_slist* slist = NULL;

    cpl_propertylist* properties = NULL;

    cpl_matrix* biasareas = NULL;

    cpl_frame* science_frame = NULL;
    cpl_frame* mbias_frame = NULL;
    cpl_frame* mdark_frame = NULL;
    cpl_frame* bpixel_frame = NULL;
    cpl_frame* slight_frame = NULL;
    cpl_frame* locy_frame = NULL;
    cpl_frame* locw_frame = NULL;
    cpl_frame* psfdata_frame = NULL;
    cpl_frame* grating_frame = NULL;
    cpl_frame* linemask_frame = NULL;
    cpl_frame* slit_frame = NULL;
    cpl_frame* wcal_frame = NULL;
    cpl_frame* rscience_frame = NULL;
    cpl_frame* sext_frame = NULL;
    cpl_frame* rbin_frame = NULL;

    cpl_parameter* p = NULL;

    GiImage* mbias = NULL;
    GiImage* mdark = NULL;
    GiImage* bpixel = NULL;
    GiImage* slight = NULL;
    GiImage* sscience = NULL;
    GiImage* rscience = NULL;

    GiTable* fibers = NULL;
    GiTable* slitgeometry = NULL;
    GiTable* grating = NULL;
    GiTable* wcalcoeff = NULL;

    GiLocalization* localization = NULL;
    GiExtraction* extraction = NULL;
    GiRebinning* rebinning = NULL;

    GiBiasConfig* bias_config = NULL;
    GiExtractConfig* extract_config = NULL;
    GiFlatConfig* flat_config = NULL;
    GiRebinConfig* rebin_config = NULL;

    GiInstrumentMode mode;

    GiRecipeInfo info = {(cxchar*)_id, 1, NULL, config};

    GiGroupInfo groups[] = {
        {GIFRAME_SCIENCE, CPL_FRAME_GROUP_RAW},
        {GIFRAME_BADPIXEL_MAP, CPL_FRAME_GROUP_CALIB},
        {GIFRAME_BIAS_MASTER, CPL_FRAME_GROUP_CALIB},
        {GIFRAME_DARK_MASTER, CPL_FRAME_GROUP_CALIB},
        {GIFRAME_FIBER_FLAT_EXTSPECTRA, CPL_FRAME_GROUP_CALIB},
        {GIFRAME_FIBER_FLAT_EXTERRORS, CPL_FRAME_GROUP_CALIB},
        {GIFRAME_SCATTERED_LIGHT_MODEL, CPL_FRAME_GROUP_CALIB},
        {GIFRAME_LOCALIZATION_CENTROID, CPL_FRAME_GROUP_CALIB},
        {GIFRAME_LOCALIZATION_WIDTH, CPL_FRAME_GROUP_CALIB},
        {GIFRAME_PSF_CENTROID, CPL_FRAME_GROUP_CALIB},
        {GIFRAME_PSF_WIDTH, CPL_FRAME_GROUP_CALIB},
        {GIFRAME_PSF_DATA, CPL_FRAME_GROUP_CALIB},
        {GIFRAME_WAVELENGTH_SOLUTION, CPL_FRAME_GROUP_CALIB},
        {GIFRAME_LINE_MASK, CPL_FRAME_GROUP_CALIB},
        {GIFRAME_SLITSETUP, CPL_FRAME_GROUP_CALIB},
        {GIFRAME_SLITMASTER, CPL_FRAME_GROUP_CALIB},
        {GIFRAME_GRATING, CPL_FRAME_GROUP_CALIB},
        {NULL, CPL_FRAME_GROUP_NONE}
    };



    if (!config) {
        cpl_msg_error(_id, "Invalid parameter list! Aborting ...");
        return 1;
    }

    if (!set) {
        cpl_msg_error(_id, "Invalid frame set! Aborting ...");
        return 1;
    }

    status = giraffe_frameset_set_groups(set, groups);

    if (status != 0) {
        cpl_msg_error(_id, "Setting frame group information failed!");
        return 1;
    }


    /*
     * Verify the frame set contents
     */

    nscience = cpl_frameset_count_tags(set, GIFRAME_SCIENCE);

    if (nscience < 1) {
        cpl_msg_error(_id, "Too few (%ld) raw frames (%s) present in "
                      "frame set! Aborting ...", nscience, GIFRAME_SCIENCE);
        return 1;
    }

    locy_frame = cpl_frameset_find(set, GIFRAME_PSF_CENTROID);

    if (locy_frame == NULL) {

        locy_frame = cpl_frameset_find(set, GIFRAME_LOCALIZATION_CENTROID);

        if (locy_frame == NULL) {
            cpl_msg_info(_id, "No master localization (centroid position) "
                         "present in frame set. Aborting ...");
            return 1;
        }

    }

    locw_frame = cpl_frameset_find(set, GIFRAME_PSF_WIDTH);

    if (locw_frame == NULL) {

        locw_frame = cpl_frameset_find(set, GIFRAME_LOCALIZATION_WIDTH);

        if (locw_frame == NULL) {
            cpl_msg_info(_id, "No master localization (spectrum width) "
                         "present in frame set. Aborting ...");
            return 1;
        }

    }

    grating_frame = cpl_frameset_find(set, GIFRAME_GRATING);

    if (!grating_frame) {
        cpl_msg_error(_id, "No grating data present in frame set. "
                      "Aborting ...");
        return 1;
    }

    slit_frame = giraffe_get_slitgeometry(set);

    if (!slit_frame) {
        cpl_msg_error(_id, "No slit geometry present in frame set. "
                      "Aborting ...");
        return 1;
    }

    wcal_frame = cpl_frameset_find(set, GIFRAME_WAVELENGTH_SOLUTION);

    if (!wcal_frame) {
        cpl_msg_error(_id, "No dispersion solution present in frame set. "
                      "Aborting ...");
        return 1;
    }

    linemask_frame = cpl_frameset_find(set, GIFRAME_LINE_MASK);

    if (!linemask_frame) {
        cpl_msg_warning(_id, "No reference line mask present in frame set.");
    }

    bpixel_frame = cpl_frameset_find(set, GIFRAME_BADPIXEL_MAP);

    if (!bpixel_frame) {
        cpl_msg_info(_id, "No bad pixel map present in frame set.");
    }

    mbias_frame = cpl_frameset_find(set, GIFRAME_BIAS_MASTER);

    if (!mbias_frame) {
        cpl_msg_info(_id, "No master bias present in frame set.");
    }

    mdark_frame = cpl_frameset_find(set, GIFRAME_DARK_MASTER);

    if (!mdark_frame) {
        cpl_msg_info(_id, "No master dark present in frame set.");
    }

    slight_frame = cpl_frameset_find(set, GIFRAME_SCATTERED_LIGHT_MODEL);

    if (!slight_frame) {
        cpl_msg_info(_id, "No scattered light model present in frame set.");
    }

    psfdata_frame = cpl_frameset_find(set, GIFRAME_PSF_DATA);

    if (!psfdata_frame) {
        cpl_msg_info(_id, "No PSF profile parameters present in frame set.");
    }


    /*
     * Load raw images
     */

    slist = cx_slist_new();

    science_frame = cpl_frameset_find(set, GIFRAME_SCIENCE);

    for (i = 0; i < nscience; i++) {

        filename = cpl_frame_get_filename(science_frame);

        GiImage* raw = giraffe_image_new(CPL_TYPE_DOUBLE);


        status = giraffe_image_load(raw, filename, 0);

        if (status) {
            cpl_msg_error(_id, "Cannot load raw science frame from '%s'. "
                          "Aborting ...", filename);

            cx_slist_destroy(slist, (cx_free_func) giraffe_image_delete);

            return 1;
        }

        cx_slist_push_back(slist, raw);

        science_frame = cpl_frameset_find(set, NULL);

    }

    nscience = (cxint)cx_slist_size(slist);
    sscience = cx_slist_pop_front(slist);

    properties = giraffe_image_get_properties(sscience);
    cx_assert(properties != NULL);

    if (nscience > 1) {

        /*
         * Create a stacked science image from the list of raw images.
         * Each raw image is disposed when it is no longer needed.
         */

        cpl_msg_info(_id, "Averaging science frames ...");

        exptime = cpl_propertylist_get_double(properties, GIALIAS_EXPTIME);

        for (i = 1; i < nscience; i++) {

            cpl_propertylist* _properties;

            GiImage* science = cx_slist_pop_front(slist);


            cpl_image_add(giraffe_image_get(sscience),
                          giraffe_image_get(science));

            _properties = giraffe_image_get_properties(science);
            cx_assert(_properties != NULL);

            exptime += cpl_propertylist_get_double(_properties, GIALIAS_EXPTIME);

            giraffe_image_delete(science);

        }

        cpl_image_divide_scalar(giraffe_image_get(sscience), nscience);
    }

    cx_assert(cx_slist_empty(slist));
    cx_slist_delete(slist);
    slist = NULL;


    if (nscience > 1) {

        /*
         * Update stacked science image properties
         */

        cpl_msg_info(_id, "Updating stacked science image properties ...");

        cpl_propertylist_set_double(properties, GIALIAS_EXPTIME,
                                    exptime / nscience);

        cpl_propertylist_append_double(properties, GIALIAS_EXPTTOT, exptime);
        cpl_propertylist_set_comment(properties, GIALIAS_EXPTTOT,
                                     "Total exposure time of all frames "
                                     "combined");

        cpl_propertylist_erase(properties, GIALIAS_TPLEXPNO);

    }

    cpl_propertylist_append_int(properties, GIALIAS_DATANCOM, nscience);
    cpl_propertylist_set_comment(properties, GIALIAS_DATANCOM,
                                 "Number of combined frames");



    /*
     * Prepare for bias subtraction
     */

    bias_config = giraffe_bias_config_create(config);

    /*
     * Setup user defined areas to use for the bias computation
     */

    if (bias_config->method == GIBIAS_METHOD_MASTER ||
        bias_config->method == GIBIAS_METHOD_ZMASTER) {

        if (!mbias_frame) {
            cpl_msg_error(_id, "Missing master bias frame! Selected bias "
                          "removal method requires a master bias frame!");

            giraffe_bias_config_destroy(bias_config);
            giraffe_image_delete(sscience);

            return 1;
        }
        else {
            filename = cpl_frame_get_filename(mbias_frame);


            mbias = giraffe_image_new(CPL_TYPE_DOUBLE);
            status = giraffe_image_load(mbias, filename, 0);

            if (status) {
                cpl_msg_error(_id, "Cannot load master bias from '%s'. "
                              "Aborting ...", filename);

                giraffe_bias_config_destroy(bias_config);
                giraffe_image_delete(sscience);

                return 1;
            }
        }
    }


    /*
     * Load bad pixel map if it is present in the frame set.
     */

    if (bpixel_frame) {

        filename = cpl_frame_get_filename(bpixel_frame);


        bpixel = giraffe_image_new(CPL_TYPE_INT);
        status = giraffe_image_load(bpixel, filename, 0);

        if (status) {
            cpl_msg_error(_id, "Cannot load bad pixel map from '%s'. "
                          "Aborting ...", filename);

            giraffe_image_delete(bpixel);
            bpixel = NULL;

            if (mbias != NULL) {
                giraffe_image_delete(mbias);
                mbias = NULL;
            }

            giraffe_bias_config_destroy(bias_config);
            bias_config = NULL;

            giraffe_image_delete(sscience);
            sscience = NULL;

            return 1;
        }

    }


    /*
     * Compute and remove the bias from the stacked flat field frame.
     */

    rscience = giraffe_image_new(CPL_TYPE_DOUBLE);

    status = giraffe_bias_remove(rscience, sscience, mbias, bpixel, biasareas,
                                 bias_config);

    giraffe_image_delete(sscience);

    if (mbias) {
        giraffe_image_delete(mbias);
        mbias = NULL;
    }

    giraffe_bias_config_destroy(bias_config);

    if (status) {
        cpl_msg_error(_id, "Bias removal failed. Aborting ...");

        giraffe_image_delete(rscience);
        rscience = NULL;

        if (bpixel != NULL) {
            giraffe_image_delete(bpixel);
            bpixel = NULL;
        }

        return 1;
    }


    /*
     * Load master dark if it is present in the frame set and correct
     * the master flat field for the dark current.
     */

    if (mdark_frame) {

        GiDarkConfig dark_config = {GIDARK_METHOD_ZMASTER, 0.};


        cpl_msg_info(_id, "Correcting for dark current ...");

        filename = cpl_frame_get_filename(mdark_frame);

        mdark = giraffe_image_new(CPL_TYPE_DOUBLE);
        status = giraffe_image_load(mdark, filename, 0);

        if (status != 0) {
            cpl_msg_error(_id, "Cannot load master dark from '%s'. "
                          "Aborting ...", filename);

            giraffe_image_delete(rscience);
            rscience = NULL;

            if (bpixel != NULL) {
                giraffe_image_delete(bpixel);
                bpixel = NULL;
            }

            return 1;
        }

        status = giraffe_subtract_dark(rscience, mdark, bpixel, NULL,
                                       &dark_config);

        if (status != 0) {
            cpl_msg_error(_id, "Dark subtraction failed! Aborting ...");

            giraffe_image_delete(mdark);
            mdark = NULL;

            giraffe_image_delete(rscience);
            rscience = NULL;

            if (bpixel != NULL) {
                giraffe_image_delete(bpixel);
                bpixel = NULL;
            }

            return 1;
        }

        giraffe_image_delete(mdark);
        mdark = NULL;

    }


    /*
     * Update the reduced science properties, save the reduced science frame
     * and register it as product.
     */

    cpl_msg_info(_id, "Writing pre-processed science image ...");

    giraffe_image_add_info(rscience, &info, set);

    rscience_frame = giraffe_frame_create_image(rscience,
                                             GIFRAME_SCIENCE_REDUCED,
                                             CPL_FRAME_LEVEL_INTERMEDIATE,
                                             TRUE, TRUE);

    if (rscience_frame == NULL) {
        cpl_msg_error(_id, "Cannot create local file! Aborting ...");

        giraffe_image_delete(rscience);

        return 1;
    }

    cpl_frameset_insert(set, rscience_frame);


    /*
     * Determine fiber setup
     */

    science_frame = cpl_frameset_find(set, GIFRAME_SCIENCE);

    cpl_msg_info(_id, "Building fiber setup for frame '%s'.",
                 cpl_frame_get_filename(science_frame));

    fibers = giraffe_fibers_setup(science_frame, locy_frame);

    if (!fibers) {
        cpl_msg_error(_id, "Cannot create fiber setup for frame '%s'! "
                      "Aborting ...", cpl_frame_get_filename(science_frame));

        if (bpixel) {
            giraffe_image_delete(bpixel);
            bpixel = NULL;
        }

        giraffe_image_delete(rscience);
        rscience = NULL;

        return 1;
    }

    cpl_msg_info(_id, "Fiber reference setup taken from localization "
                 "frame '%s'.", cpl_frame_get_filename(locy_frame));


    /*
     * Load fiber localization
     */

    localization = giraffe_localization_new();

    filename = cpl_frame_get_filename(locy_frame);
    status = 0;

    localization->locy  = giraffe_image_new(CPL_TYPE_DOUBLE);
    status = giraffe_image_load(localization->locy, filename, 0);

    if (status) {
        cpl_msg_error(_id, "Cannot load localization (centroid "
                      "position) frame from '%s'. Aborting ...",
                      filename);

        giraffe_localization_destroy(localization);

        if (bpixel) {
            giraffe_image_delete(bpixel);
            bpixel = NULL;
        }

        giraffe_table_delete(fibers);
        giraffe_image_delete(rscience);

        return 1;
    }


    filename = cpl_frame_get_filename(locw_frame);
    status = 0;

    localization->locw  = giraffe_image_new(CPL_TYPE_DOUBLE);
    status = giraffe_image_load(localization->locw, filename, 0);

    if (status) {
        cpl_msg_error(_id, "Cannot load localization (spectrum width) "
                      "frame from '%s'. Aborting ...", filename);

        giraffe_localization_destroy(localization);

        if (bpixel) {
            giraffe_image_delete(bpixel);
            bpixel = NULL;
        }

        giraffe_table_delete(fibers);
        giraffe_image_delete(rscience);

        return 1;
    }


    /*
     * Spectrum extraction
     */

    if (slight_frame) {

        filename = cpl_frame_get_filename(slight_frame);


        slight = giraffe_image_new(CPL_TYPE_DOUBLE);
        status = giraffe_image_load(slight, filename, 0);

        if (status) {
            cpl_msg_error(_id, "Cannot load scattered light model from '%s'. "
                          "Aborting ...", filename);

            giraffe_image_delete(slight);

            giraffe_localization_destroy(localization);

            if (bpixel) {
                giraffe_image_delete(bpixel);
                bpixel = NULL;
            }

            giraffe_table_delete(fibers);
            giraffe_image_delete(rscience);

            return 1;

        }

    }


    extract_config = giraffe_extract_config_create(config);

    if ((extract_config->emethod == GIEXTRACT_OPTIMAL) ||
        (extract_config->emethod == GIEXTRACT_HORNE)) {

        if (psfdata_frame == NULL) {

            const cxchar* emethod = "Optimal";

            if (extract_config->emethod == GIEXTRACT_HORNE) {
                emethod = "Horne";
            }

            cpl_msg_error(_id, "%s spectrum extraction requires PSF "
                          "profile data. Aborting ...", emethod);

            giraffe_extract_config_destroy(extract_config);
            extract_config = NULL;

            if (slight != NULL) {
                giraffe_image_delete(slight);
                slight = NULL;
            }

            giraffe_localization_destroy(localization);
            localization = NULL;

            if (bpixel) {
                giraffe_image_delete(bpixel);
                bpixel = NULL;
            }

            giraffe_table_delete(fibers);
            fibers = NULL;

            giraffe_image_delete(rscience);
            rscience = NULL;

            return 1;

        }
        else {

            filename = cpl_frame_get_filename(psfdata_frame);
            status = 0;

            localization->psf  = giraffe_psfdata_new();
            status = giraffe_psfdata_load(localization->psf, filename);

            if (status) {
                cpl_msg_error(_id, "Cannot load PSF profile data frame from "
                              "'%s'. Aborting ...", filename);

                giraffe_extract_config_destroy(extract_config);
                extract_config = NULL;

                if (slight != NULL) {
                    giraffe_image_delete(slight);
                    slight = NULL;
                }

                giraffe_localization_destroy(localization);
                localization = NULL;

                if (bpixel) {
                    giraffe_image_delete(bpixel);
                    bpixel = NULL;
                }

                giraffe_table_delete(fibers);
                fibers = NULL;

                giraffe_image_delete(rscience);
                rscience = NULL;

                return 1;

            }

        }

    }


    extraction = giraffe_extraction_new();

    status = giraffe_extract_spectra(extraction, rscience, fibers,
                                     localization, bpixel, slight,
                                     extract_config);

    if (status) {
        cpl_msg_error(_id, "Spectrum extraction failed! Aborting ...");

        giraffe_extraction_destroy(extraction);
        giraffe_extract_config_destroy(extract_config);

        giraffe_image_delete(slight);

        giraffe_localization_destroy(localization);

        if (bpixel) {
            giraffe_image_delete(bpixel);
            bpixel = NULL;
        }

        giraffe_table_delete(fibers);
        giraffe_image_delete(rscience);

        return 1;
    }

    giraffe_image_delete(slight);
    slight = NULL;

    if (bpixel) {
        giraffe_image_delete(bpixel);
        bpixel = NULL;
    }

    giraffe_image_delete(rscience);
    rscience = NULL;

    giraffe_extract_config_destroy(extract_config);


    /*
     * Apply flat field and apply the relative fiber transmission correction.
     */

    flat_config = giraffe_flat_config_create(config);

    if (flat_config->load == TRUE) {

        cpl_frame* flat_frame = NULL;

        GiImage* flat = NULL;


        flat_frame = cpl_frameset_find(set, GIFRAME_FIBER_FLAT_EXTSPECTRA);

        if (flat_frame == NULL) {
            cpl_msg_error(_id, "Missing flat field spectra frame!");

            giraffe_flat_config_destroy(flat_config);

            giraffe_extraction_destroy(extraction);
            giraffe_localization_destroy(localization);

            giraffe_table_delete(wcalcoeff);

            giraffe_table_delete(grating);
            giraffe_table_delete(fibers);

            return 1;
        }

        filename = cpl_frame_get_filename(flat_frame);

        flat = giraffe_image_new(CPL_TYPE_DOUBLE);
        status = giraffe_image_load(flat, filename, 0);

        if (status) {
            cpl_msg_error(_id, "Cannot load flat field spectra from '%s'. "
                          "Aborting ...", filename);

            giraffe_image_delete(flat);

            giraffe_flat_config_destroy(flat_config);

            giraffe_extraction_destroy(extraction);
            giraffe_localization_destroy(localization);

            giraffe_table_delete(wcalcoeff);

            giraffe_table_delete(grating);
            giraffe_table_delete(fibers);

            return 1;
        }

        if (flat_config->apply == TRUE) {

            GiImage* errors = NULL;


            flat_frame = cpl_frameset_find(set, GIFRAME_FIBER_FLAT_EXTERRORS);

            if (flat_frame == NULL) {
                cpl_msg_warning(_id, "Missing flat field spectra errors "
                                "frame!");
            }
            else {

                filename = cpl_frame_get_filename(flat_frame);

                errors = giraffe_image_new(CPL_TYPE_DOUBLE);
                status = giraffe_image_load(errors, filename, 0);

                if (status) {
                    cpl_msg_error(_id, "Cannot load flat field spectra "
                                  "errors from '%s'. Aborting ...",
                                  filename);

                    giraffe_image_delete(errors);
                    giraffe_image_delete(flat);

                    giraffe_flat_config_destroy(flat_config);

                    giraffe_extraction_destroy(extraction);
                    giraffe_localization_destroy(localization);

                    giraffe_table_delete(wcalcoeff);

                    giraffe_table_delete(grating);
                    giraffe_table_delete(fibers);

                    return 1;
                }

            }

            cpl_msg_info(_id, "Applying flat field correction ...");

            status = giraffe_flat_apply(extraction, fibers, flat, errors,
                                        flat_config);

            if (status) {
                cpl_msg_error(_id, "Flat field correction failed! "
                              "Aborting ...");

                giraffe_image_delete(errors);
                giraffe_image_delete(flat);

                giraffe_flat_config_destroy(flat_config);

                giraffe_extraction_destroy(extraction);
                giraffe_localization_destroy(localization);

                giraffe_table_delete(wcalcoeff);

                giraffe_table_delete(grating);
                giraffe_table_delete(fibers);

                return 1;
            }

            giraffe_image_delete(errors);
            errors = NULL;

        }

        if (flat_config->transmission == TRUE) {

            const cxchar* _filename = cpl_frame_get_filename(flat_frame);

            GiTable* _fibers = NULL;


            cpl_msg_info(_id, "Loading fiber setup for frame '%s'.",
                         _filename);

            _fibers = giraffe_fiberlist_load(_filename, 1, "FIBER_SETUP");

            if (!_fibers) {
                cpl_msg_error(_id, "Cannot create fiber setup for "
                              "frame '%s'! Aborting ...", _filename);

                giraffe_image_delete(flat);

                giraffe_flat_config_destroy(flat_config);

                giraffe_extraction_destroy(extraction);
                giraffe_localization_destroy(localization);

                giraffe_table_delete(wcalcoeff);

                giraffe_table_delete(grating);
                giraffe_table_delete(fibers);

                return 1;
            }

            cpl_msg_info(_id, "Applying relative fiber transmission "
                         "correction");

            status = giraffe_transmission_setup(fibers, _fibers);
            giraffe_table_delete(_fibers);

            if (status == 0) {
                status = giraffe_transmission_apply(extraction, fibers);
            }

            if (status) {

                cpl_msg_error(_id, "Relative transmission correction failed! "
                              "Aborting ...");

                giraffe_image_delete(flat);

                giraffe_flat_config_destroy(flat_config);

                giraffe_extraction_destroy(extraction);
                giraffe_localization_destroy(localization);

                giraffe_table_delete(wcalcoeff);

                giraffe_table_delete(grating);
                giraffe_table_delete(fibers);

                return 1;

            }

        }

        giraffe_image_delete(flat);

    }

    giraffe_flat_config_destroy(flat_config);


    /*
     * Save the spectrum extraction results and register them as
     * products.
     */

    cpl_msg_info(_id, "Writing extracted spectra ...");

    /* Extracted spectra */

    giraffe_image_add_info(extraction->spectra, &info, set);

    sext_frame = giraffe_frame_create_image(extraction->spectra,
                                            GIFRAME_SCIENCE_EXTSPECTRA,
                                            CPL_FRAME_LEVEL_FINAL,
                                            TRUE, TRUE);

    if (sext_frame == NULL) {
        cpl_msg_error(_id, "Cannot create local file! Aborting ...");

        giraffe_extraction_destroy(extraction);
        giraffe_localization_destroy(localization);

        giraffe_table_delete(wcalcoeff);

        giraffe_table_delete(grating);
        giraffe_table_delete(fibers);

        return 1;
    }

    status = giraffe_fiberlist_attach(sext_frame, fibers);

    if (status) {
        cpl_msg_error(_id, "Cannot attach fiber setup to local file '%s'! "
                      "Aborting ...", cpl_frame_get_filename(sext_frame));

        cpl_frame_delete(sext_frame);

        giraffe_extraction_destroy(extraction);
        giraffe_localization_destroy(localization);

        giraffe_table_delete(wcalcoeff);

        giraffe_table_delete(grating);
        giraffe_table_delete(fibers);

        return 1;
    }

    cpl_frameset_insert(set, sext_frame);

    /* Extracted spectra errors */

    giraffe_image_add_info(extraction->error, &info, set);

    sext_frame = giraffe_frame_create_image(extraction->error,
                                            GIFRAME_SCIENCE_EXTERRORS,
                                            CPL_FRAME_LEVEL_FINAL,
                                            TRUE, TRUE);

    if (sext_frame == NULL) {
        cpl_msg_error(_id, "Cannot create local file! Aborting ...");

        giraffe_extraction_destroy(extraction);
        giraffe_localization_destroy(localization);

        giraffe_table_delete(wcalcoeff);

        giraffe_table_delete(grating);
        giraffe_table_delete(fibers);

        return 1;
    }

    status = giraffe_fiberlist_attach(sext_frame, fibers);

    if (status) {
        cpl_msg_error(_id, "Cannot attach fiber setup to local file '%s'! "
                      "Aborting ...", cpl_frame_get_filename(sext_frame));

        cpl_frame_delete(sext_frame);

        giraffe_extraction_destroy(extraction);
        giraffe_localization_destroy(localization);

        giraffe_table_delete(wcalcoeff);

        giraffe_table_delete(grating);
        giraffe_table_delete(fibers);

        return 1;
    }

    cpl_frameset_insert(set, sext_frame);

    /* Extracted spectra pixels */

    if (extraction->npixels != NULL) {

        giraffe_image_add_info(extraction->npixels, &info, set);

        sext_frame = giraffe_frame_create_image(extraction->npixels,
                                                GIFRAME_SCIENCE_EXTPIXELS,
                                                CPL_FRAME_LEVEL_FINAL,
                                                TRUE, TRUE);

        if (sext_frame == NULL) {
            cpl_msg_error(_id, "Cannot create local file! Aborting ...");

            giraffe_extraction_destroy(extraction);
            giraffe_localization_destroy(localization);

            giraffe_table_delete(wcalcoeff);

            giraffe_table_delete(grating);
            giraffe_table_delete(fibers);

            return 1;
        }

        status = giraffe_fiberlist_attach(sext_frame, fibers);

        if (status) {
            cpl_msg_error(_id, "Cannot attach fiber setup to local file '%s'! "
                        "Aborting ...", cpl_frame_get_filename(sext_frame));

            cpl_frame_delete(sext_frame);

            giraffe_extraction_destroy(extraction);
            giraffe_localization_destroy(localization);

            giraffe_table_delete(wcalcoeff);

            giraffe_table_delete(grating);
            giraffe_table_delete(fibers);

            return 1;
        }

        cpl_frameset_insert(set, sext_frame);

    }

    /* Extracted spectra centroids */

    giraffe_image_add_info(extraction->centroid, &info, set);

    sext_frame = giraffe_frame_create_image(extraction->centroid,
                                            GIFRAME_SCIENCE_EXTTRACE,
                                            CPL_FRAME_LEVEL_FINAL,
                                            TRUE, TRUE);

    if (sext_frame == NULL) {
        cpl_msg_error(_id, "Cannot create local file! Aborting ...");

        giraffe_extraction_destroy(extraction);
        giraffe_localization_destroy(localization);

        giraffe_table_delete(wcalcoeff);

        giraffe_table_delete(grating);
        giraffe_table_delete(fibers);

        return 1;
    }

    status = giraffe_fiberlist_attach(sext_frame, fibers);

    if (status) {
        cpl_msg_error(_id, "Cannot attach fiber setup to local file '%s'! "
                      "Aborting ...", cpl_frame_get_filename(sext_frame));

        cpl_frame_delete(sext_frame);

        giraffe_extraction_destroy(extraction);
        giraffe_localization_destroy(localization);

        giraffe_table_delete(wcalcoeff);

        giraffe_table_delete(grating);
        giraffe_table_delete(fibers);

        return 1;
    }

    cpl_frameset_insert(set, sext_frame);

    /* Extraction model spectra */

    if (extraction->model != NULL) {

        giraffe_image_add_info(extraction->model, &info, set);

        sext_frame = giraffe_frame_create_image(extraction->model,
                                                GIFRAME_SCIENCE_EXTMODEL,
                                                CPL_FRAME_LEVEL_FINAL,
                                                TRUE, TRUE);

        if (sext_frame == NULL) {
            cpl_msg_error(_id, "Cannot create local file! Aborting ...");

            giraffe_extraction_destroy(extraction);
            giraffe_localization_destroy(localization);

            giraffe_table_delete(wcalcoeff);

            giraffe_table_delete(grating);
            giraffe_table_delete(fibers);

            return 1;
        }

        status = giraffe_fiberlist_attach(sext_frame, fibers);

        if (status != 0) {
            cpl_msg_error(_id, "Cannot attach fiber setup to local file '%s'! "
                          "Aborting ...", cpl_frame_get_filename(sext_frame));

            cpl_frame_delete(sext_frame);

            giraffe_extraction_destroy(extraction);
            giraffe_localization_destroy(localization);

            giraffe_table_delete(wcalcoeff);

            giraffe_table_delete(grating);
            giraffe_table_delete(fibers);

            return 1;
        }

        cpl_frameset_insert(set, sext_frame);

    }


    /*
     * Load dispersion solution
     */


    filename = (cxchar *)cpl_frame_get_filename(wcal_frame);

    wcalcoeff = giraffe_table_new();
    status = giraffe_table_load(wcalcoeff, filename, 1, NULL);

    if (status) {
        cpl_msg_error(_id, "Cannot load dispersion solution from "
                      "'%s'. Aborting ...", filename);

        giraffe_extraction_destroy(extraction);
        giraffe_localization_destroy(localization);

        giraffe_table_delete(wcalcoeff);

        giraffe_table_delete(grating);
        giraffe_table_delete(fibers);

        return 1;
    }


    /*
     * Load grating data
     */

    filename = (cxchar *)cpl_frame_get_filename(grating_frame);

    status = 0;

    grating = giraffe_table_new();
    status = giraffe_table_load(grating, filename, 1, NULL);

    if (status) {
        cpl_msg_error(_id, "Cannot load grating data from '%s'. "
                      "Aborting ...", filename);

        giraffe_extraction_destroy(extraction);
        giraffe_localization_destroy(localization);

        giraffe_table_delete(wcalcoeff);

        giraffe_table_delete(grating);
        giraffe_table_delete(fibers);

        return 1;
    }


    /*
     * Load slit geometry data
     */


    filename = (cxchar *)cpl_frame_get_filename(slit_frame);

    slitgeometry = giraffe_slitgeometry_load(fibers, filename, 1, NULL);

    if (slitgeometry == NULL) {
        cpl_msg_error(_id, "Cannot load slit geometry data from '%s'. "
                      "Aborting ...", filename);

        giraffe_table_delete(wcalcoeff);

        giraffe_extraction_destroy(extraction);
        giraffe_localization_destroy(localization);

        giraffe_table_delete(wcalcoeff);

        giraffe_table_delete(grating);
        giraffe_table_delete(fibers);

        return 1;
    }
    else {

        /*
         * Check whether the contains the positions for all fibers
         * provided by the fiber setup. If this is not the case
         * this is an error.
         */

        if (giraffe_fiberlist_compare(slitgeometry, fibers) != 1) {
            cpl_msg_error(_id, "Slit geometry data from '%s' is not "
                    "applicable for current fiber setup! "
                            "Aborting ...", filename);

            giraffe_table_delete(slitgeometry);
            giraffe_table_delete(wcalcoeff);

            giraffe_extraction_destroy(extraction);
            giraffe_localization_destroy(localization);

            giraffe_table_delete(wcalcoeff);

            giraffe_table_delete(grating);
            giraffe_table_delete(fibers);

            return 1;
        }

    }



    /*
     * Spectrum rebinning
     */

    cpl_msg_info(_id, "Spectrum rebinning");

    rebin_config = giraffe_rebin_config_create(config);

    rebinning = giraffe_rebinning_new();

    status = giraffe_rebin_spectra(rebinning, extraction, fibers,
                                   localization, grating, slitgeometry,
                                   wcalcoeff, rebin_config);

    if (status) {
        cpl_msg_error(_id, "Rebinning of science spectra failed! Aborting...");

        giraffe_rebinning_destroy(rebinning);

        giraffe_extraction_destroy(extraction);
        giraffe_localization_destroy(localization);

        giraffe_table_delete(wcalcoeff);

        giraffe_table_delete(slitgeometry);
        giraffe_table_delete(grating);
        giraffe_table_delete(fibers);

        giraffe_rebin_config_destroy(rebin_config);

        return 1;

    }


    /*
     * Optionally compute and apply spectral shifts from the simultaneous
     * calibration fibers. This is only done if the simultaneous calibration
     * fibers were used.
     */

    p = cpl_parameterlist_find(config, "giraffe.siwc.apply");
    cx_assert(p != NULL);

    siwc = cpl_parameter_get_bool(p);
    p = NULL;

    properties = giraffe_image_get_properties(rebinning->spectra);
    cx_assert(properties != NULL);


    if (cpl_propertylist_has(properties, GIALIAS_STSCTAL) == TRUE) {
        calsim = cpl_propertylist_get_bool(properties, GIALIAS_STSCTAL);
    }


    if ((siwc == TRUE) && (calsim == TRUE) && (linemask_frame != NULL)) {

        GiTable* linemask = giraffe_table_new();

        GiSGCalConfig* siwc_config = NULL;


        siwc_config = giraffe_sgcalibration_config_create(config);

        if (siwc_config == NULL) {

            giraffe_table_delete(linemask);
            linemask = NULL;

            giraffe_rebinning_destroy(rebinning);

            giraffe_extraction_destroy(extraction);
            giraffe_localization_destroy(localization);

            giraffe_table_delete(wcalcoeff);

            giraffe_table_delete(slitgeometry);
            giraffe_table_delete(grating);
            giraffe_table_delete(fibers);

            giraffe_rebin_config_destroy(rebin_config);

            return 1;

        }

        filename = cpl_frame_get_filename(linemask_frame);

        status = giraffe_table_load(linemask, filename, 1, NULL);

        if (status) {
            cpl_msg_error(_id, "Cannot load line reference mask from '%s'. "
                          "Aborting ...", filename);

            giraffe_sgcalibration_config_destroy(siwc_config);
            siwc_config = NULL;

            giraffe_table_delete(linemask);
            linemask = NULL;

            giraffe_rebinning_destroy(rebinning);

            giraffe_extraction_destroy(extraction);
            giraffe_localization_destroy(localization);

            giraffe_table_delete(wcalcoeff);

            giraffe_table_delete(slitgeometry);
            giraffe_table_delete(grating);
            giraffe_table_delete(fibers);

            giraffe_rebin_config_destroy(rebin_config);

            return 1;

        }


        status = giraffe_compute_offsets(fibers, rebinning, grating,
                                         linemask, siwc_config);

        if (status != 0) {
            cpl_msg_error(_id, "Applying simultaneous wavelength "
                          "calibration correction failed! Aborting...");

            giraffe_sgcalibration_config_destroy(siwc_config);
            siwc_config = NULL;

            giraffe_table_delete(linemask);
            linemask = NULL;

            giraffe_rebinning_destroy(rebinning);

            giraffe_extraction_destroy(extraction);
            giraffe_localization_destroy(localization);

            giraffe_table_delete(wcalcoeff);

            giraffe_table_delete(slitgeometry);
            giraffe_table_delete(grating);
            giraffe_table_delete(fibers);

            giraffe_rebin_config_destroy(rebin_config);

            return 1;

        }

        giraffe_sgcalibration_config_destroy(siwc_config);
        siwc_config = NULL;

        giraffe_table_delete(linemask);
        linemask = NULL;

        giraffe_rebinning_destroy(rebinning);
        rebinning = giraffe_rebinning_new();

        status = giraffe_rebin_spectra(rebinning, extraction, fibers,
                                       localization, grating, slitgeometry,
                                       wcalcoeff, rebin_config);

        if (status) {
            cpl_msg_error(_id, "Rebinning of science spectra failed! "
                          "Aborting...");

            giraffe_rebinning_destroy(rebinning);

            giraffe_extraction_destroy(extraction);
            giraffe_localization_destroy(localization);

            giraffe_table_delete(wcalcoeff);

            giraffe_table_delete(slitgeometry);
            giraffe_table_delete(grating);
            giraffe_table_delete(fibers);

            giraffe_rebin_config_destroy(rebin_config);

            return 1;

        }

    }

    giraffe_extraction_destroy(extraction);
    extraction = NULL;

    giraffe_localization_destroy(localization);
    localization = NULL;

    giraffe_rebin_config_destroy(rebin_config);
    rebin_config = NULL;


    /*
     * Compute barycentric correction for each object spectrum (fiber)
     */

    status = giraffe_add_rvcorrection(fibers, rebinning->spectra);

    switch (status) {
    case 0:
        {
            break;
        }

    case 1:
        {
            cpl_msg_warning(_id, "Missing observation time properties! "
                            "Barycentric correction computation "
                            "skipped!");
            status = 0;
            break;
        }
    case 2:
        {
            cpl_msg_warning(_id, "Missing telescope location properties! "
                            "Barycentric correction computation "
                            "skipped!");
            status = 0;
            break;
        }
    case 3:
        {
            cpl_msg_warning(_id, "Object positions are not available "
                            "Barycentric correction computation "
                            "skipped!");
            status = 0;
            break;
        }
    default:
        {
            cpl_msg_error(_id, "Barycentric correction computation "
                          "failed! Aborting...");

            giraffe_rebinning_destroy(rebinning);

            giraffe_table_delete(wcalcoeff);

            giraffe_table_delete(slitgeometry);
            giraffe_table_delete(grating);
            giraffe_table_delete(fibers);

            return 1;
            break;
        }

    }


    /*
     * Save and register the results of the spectrum rebinning.
     */

    /* Rebinned spectra */

    giraffe_image_add_info(rebinning->spectra, &info, set);

    rbin_frame = giraffe_frame_create_image(rebinning->spectra,
                                            GIFRAME_SCIENCE_RBNSPECTRA,
                                            CPL_FRAME_LEVEL_FINAL,
                                            TRUE, TRUE);

    if (rbin_frame == NULL) {
        cpl_msg_error(_id, "Cannot create local file! Aborting ...");

        giraffe_rebinning_destroy(rebinning);

        giraffe_table_delete(wcalcoeff);

        giraffe_table_delete(slitgeometry);
        giraffe_table_delete(grating);
        giraffe_table_delete(fibers);

        return 1;
    }

    status = giraffe_fiberlist_attach(rbin_frame, fibers);

    if (status) {
        cpl_msg_error(_id, "Cannot attach fiber setup to local "
                      "file '%s'! Aborting ...",
                      cpl_frame_get_filename(rbin_frame));

        giraffe_rebinning_destroy(rebinning);
        giraffe_table_delete(wcalcoeff);

        giraffe_table_delete(slitgeometry);
        giraffe_table_delete(grating);
        giraffe_table_delete(fibers);

        cpl_frame_delete(rbin_frame);

        return 1;
    }

    cpl_frameset_insert(set, rbin_frame);
    flux_filename = cpl_strdup(cpl_frame_get_filename(rbin_frame));

    /* Rebinned spectra errors */

    giraffe_image_add_info(rebinning->errors, &info, set);

    rbin_frame = giraffe_frame_create_image(rebinning->errors,
                                            GIFRAME_SCIENCE_RBNERRORS,
                                            CPL_FRAME_LEVEL_FINAL,
                                            TRUE, TRUE);

    if (rbin_frame == NULL) {
        cpl_msg_error(_id, "Cannot create local file! Aborting ...");

        giraffe_rebinning_destroy(rebinning);

        giraffe_table_delete(wcalcoeff);

        giraffe_table_delete(slitgeometry);
        giraffe_table_delete(grating);
        giraffe_table_delete(fibers);
        cpl_free(flux_filename);

        return 1;
    }

    status = giraffe_fiberlist_attach(rbin_frame, fibers);

    if (status) {
        cpl_msg_error(_id, "Cannot attach fiber setup to local "
                      "file '%s'! Aborting ...",
                      cpl_frame_get_filename(rbin_frame));

        giraffe_rebinning_destroy(rebinning);

        giraffe_table_delete(wcalcoeff);

        giraffe_table_delete(slitgeometry);
        giraffe_table_delete(grating);
        giraffe_table_delete(fibers);

        cpl_frame_delete(rbin_frame);
        cpl_free(flux_filename);

        return 1;
    }

    cpl_frameset_insert(set, rbin_frame);
    err_filename = cpl_strdup(cpl_frame_get_filename(rbin_frame));

    properties = giraffe_image_get_properties(rebinning->spectra);
    mode = giraffe_get_mode(properties);

    /*
     * Optionally generate spectra in Science Data Product (SDP) format.
     */
    p = cpl_parameterlist_find(config, "giraffe.sdp.format.generate");
    cx_assert(p != NULL);
    gensdp = cpl_parameter_get_bool(p);
    p = cpl_parameterlist_find(config, "giraffe.sdp.nassoc.keys");
    cx_assert(p != NULL);
    nassoc_keys = cpl_parameter_get_int(p);
    p = NULL;
    if (gensdp) {
        if (mode == GIMODE_MEDUSA) {
            status = _giraffe_make_sdp_spectra(flux_filename, err_filename,
                                            nassoc_keys, set, config, _id);
            if (status) {
                cpl_msg_error(_id, "Failed to generate spectra in Science Data"
                              " Product format.");

                giraffe_rebinning_destroy(rebinning);

                giraffe_table_delete(wcalcoeff);

                giraffe_table_delete(slitgeometry);
                giraffe_table_delete(grating);
                giraffe_table_delete(fibers);

                cpl_free(flux_filename);
                cpl_free(err_filename);

                return 1;
            }
        } else {
            cpl_msg_warning(_id, "Requested to generate SDP 1D spectra, but"
                            " this is currently only supported for the MEDUSA"
                            " mode. Skipping SDP generation.");
        }
    }
    cpl_free(flux_filename);
    cpl_free(err_filename);


    /*
     * Optional image and data cube construction (only for IFU and Argus)
     */

    if (mode == GIMODE_IFU || mode == GIMODE_ARGUS) {

        cpl_frame* rimg_frame = NULL;

        GiFieldOfView* fov = NULL;

        GiFieldOfViewConfig* fov_config = NULL;

        GiFieldOfViewCubeFormat cube_format = GIFOV_FORMAT_ESO3D;


        fov_config = giraffe_fov_config_create(config);

        cube_format = fov_config->format;


        cpl_msg_info(_id, "Reconstructing image and data cube from rebinned "
                     "spectra ...");

        fov = giraffe_fov_new();

        status = giraffe_fov_build(fov, rebinning, fibers, wcalcoeff, grating,
                                   slitgeometry, fov_config);

        if (status) {

            if (status == -2) {
                cpl_msg_warning(_id, "No reconstructed image was built. "
                                "Fiber list has no fiber position "
                                "information.");
            }
            else {
                cpl_msg_error(_id, "Image reconstruction failed! Aborting...");

                giraffe_fov_delete(fov);
                giraffe_rebinning_destroy(rebinning);

                giraffe_table_delete(wcalcoeff);

                giraffe_table_delete(slitgeometry);
                giraffe_table_delete(grating);
                giraffe_table_delete(fibers);

                giraffe_fov_config_destroy(fov_config);

                return 1;
            }

        }

        giraffe_fov_config_destroy(fov_config);


        /*
         * Save and register the results of the image reconstruction.
         */

        /* Reconstructed image */

        giraffe_image_add_info(fov->fov.spectra, &info, set);

        rimg_frame = giraffe_frame_create_image(fov->fov.spectra,
                                                GIFRAME_SCIENCE_RCSPECTRA,
                                                CPL_FRAME_LEVEL_FINAL,
                                                TRUE, TRUE);

        if (rimg_frame == NULL) {
            cpl_msg_error(_id, "Cannot create local file! Aborting ...");

            giraffe_fov_delete(fov);
            giraffe_rebinning_destroy(rebinning);

            giraffe_table_delete(wcalcoeff);

            giraffe_table_delete(slitgeometry);
            giraffe_table_delete(grating);
            giraffe_table_delete(fibers);

            return 1;
        }

        cpl_frameset_insert(set, rimg_frame);


        /* Reconstructed image errors */

        giraffe_image_add_info(fov->fov.errors, &info, set);

        rimg_frame = giraffe_frame_create_image(fov->fov.errors,
                                                GIFRAME_SCIENCE_RCERRORS,
                                                CPL_FRAME_LEVEL_FINAL,
                                                TRUE, TRUE);

        if (rimg_frame == NULL) {
            cpl_msg_error(_id, "Cannot create local file! Aborting ...");

            giraffe_fov_delete(fov);
            giraffe_rebinning_destroy(rebinning);

            giraffe_table_delete(wcalcoeff);

            giraffe_table_delete(slitgeometry);
            giraffe_table_delete(grating);
            giraffe_table_delete(fibers);

            return 1;
        }

        cpl_frameset_insert(set, rimg_frame);


        /* Save data cubes according to format selection */

        if (cube_format == GIFOV_FORMAT_SINGLE) {

            /* Spectrum cube */

            if (fov->cubes.spectra != NULL) {

                cxint component = 0;

                GiFrameCreator creator = (GiFrameCreator) giraffe_fov_save_cubes;


                properties = giraffe_image_get_properties(rebinning->spectra);
                properties = cpl_propertylist_duplicate(properties);

                giraffe_add_frameset_info(properties, set, info.sequence);

                rimg_frame = giraffe_frame_create(GIFRAME_SCIENCE_CUBE_SPECTRA,
                                                  CPL_FRAME_LEVEL_FINAL,
                                                  properties,
                                                  fov,
                                                  &component,
                                                  creator);

                cpl_propertylist_delete(properties);
                properties = NULL;

                if (rimg_frame == NULL) {
                    cpl_msg_error(_id, "Cannot create local file! Aborting ...");

                    giraffe_fov_delete(fov);
                    fov = NULL;

                    giraffe_rebinning_destroy(rebinning);
                    rebinning = NULL;

                    giraffe_table_delete(wcalcoeff);
                    wcalcoeff = NULL;

                    giraffe_table_delete(slitgeometry);
                    slitgeometry = NULL;

                    giraffe_table_delete(grating);
                    grating = NULL;

                    giraffe_table_delete(fibers);
                    fibers = NULL;

                    return 1;
                }

                status = giraffe_fiberlist_attach(rimg_frame, fibers);

                if (status != 0) {
                    cpl_msg_error(_id, "Cannot attach fiber setup to local "
                                  "file '%s'! Aborting ...",
                                  cpl_frame_get_filename(rimg_frame));

                    cpl_frame_delete(rimg_frame);

                    giraffe_fov_delete(fov);
                    fov = NULL;

                    giraffe_rebinning_destroy(rebinning);
                    rebinning = NULL;

                    giraffe_table_delete(wcalcoeff);
                    wcalcoeff = NULL;

                    giraffe_table_delete(slitgeometry);
                    slitgeometry = NULL;

                    giraffe_table_delete(grating);
                    grating = NULL;

                    giraffe_table_delete(fibers);
                    fibers = NULL;

                    return 1;
                }

                cpl_frameset_insert(set, rimg_frame);

            }

            /* Error cube */

            if (fov->cubes.errors != NULL) {

                cxint component = 1;

                GiFrameCreator creator = (GiFrameCreator) giraffe_fov_save_cubes;


                properties = giraffe_image_get_properties(rebinning->errors);
                properties = cpl_propertylist_duplicate(properties);

                giraffe_add_frameset_info(properties, set, info.sequence);

                rimg_frame = giraffe_frame_create(GIFRAME_SCIENCE_CUBE_ERRORS,
                                                  CPL_FRAME_LEVEL_FINAL,
                                                  properties,
                                                  fov,
                                                  &component,
                                                  creator);

                cpl_propertylist_delete(properties);
                properties = NULL;

                if (rimg_frame == NULL) {
                    cpl_msg_error(_id, "Cannot create local file! Aborting ...");

                    giraffe_fov_delete(fov);
                    fov = NULL;

                    giraffe_rebinning_destroy(rebinning);
                    rebinning = NULL;

                    giraffe_table_delete(wcalcoeff);
                    wcalcoeff = NULL;

                    giraffe_table_delete(slitgeometry);
                    slitgeometry = NULL;

                    giraffe_table_delete(grating);
                    grating = NULL;

                    giraffe_table_delete(fibers);
                    fibers = NULL;

                    return 1;
                }

                status = giraffe_fiberlist_attach(rimg_frame, fibers);

                if (status != 0) {
                    cpl_msg_error(_id, "Cannot attach fiber setup to local "
                                  "file '%s'! Aborting ...",
                                  cpl_frame_get_filename(rimg_frame));

                    cpl_frame_delete(rimg_frame);

                    giraffe_fov_delete(fov);
                    fov = NULL;

                    giraffe_rebinning_destroy(rebinning);
                    rebinning = NULL;

                    giraffe_table_delete(wcalcoeff);
                    wcalcoeff = NULL;

                    giraffe_table_delete(slitgeometry);
                    slitgeometry = NULL;

                    giraffe_table_delete(grating);
                    grating = NULL;

                    giraffe_table_delete(fibers);
                    fibers = NULL;

                    return 1;
                }

                cpl_frameset_insert(set, rimg_frame);
            }

        }
        else {

            /* Data Cube (ESO 3D format) */

            GiFrameCreator creator = (GiFrameCreator) giraffe_fov_save_cubes_eso3d;

            properties = giraffe_image_get_properties(rebinning->spectra);
            properties = cpl_propertylist_duplicate(properties);

            giraffe_add_frameset_info(properties, set, info.sequence);

            rimg_frame = giraffe_frame_create(GIFRAME_SCIENCE_CUBE,
                                              CPL_FRAME_LEVEL_FINAL,
                                              properties,
                                              fov,
                                              NULL,
                                              creator);

            cpl_propertylist_delete(properties);
            properties = NULL;

            if (rimg_frame == NULL) {
                cpl_msg_error(_id, "Cannot create local file! Aborting ...");

                giraffe_fov_delete(fov);
                fov = NULL;

                giraffe_rebinning_destroy(rebinning);
                rebinning = NULL;

                giraffe_table_delete(wcalcoeff);
                wcalcoeff = NULL;

                giraffe_table_delete(slitgeometry);
                slitgeometry = NULL;

                giraffe_table_delete(grating);
                grating = NULL;

                giraffe_table_delete(fibers);
                fibers = NULL;

                return 1;
            }

            status = giraffe_fiberlist_attach(rimg_frame, fibers);

            if (status != 0) {
                cpl_msg_error(_id, "Cannot attach fiber setup to local "
                              "file '%s'! Aborting ...",
                              cpl_frame_get_filename(rimg_frame));

                cpl_frame_delete(rimg_frame);

                giraffe_fov_delete(fov);
                fov = NULL;

                giraffe_rebinning_destroy(rebinning);
                rebinning = NULL;

                giraffe_table_delete(wcalcoeff);
                wcalcoeff = NULL;

                giraffe_table_delete(slitgeometry);
                slitgeometry = NULL;

                giraffe_table_delete(grating);
                grating = NULL;

                giraffe_table_delete(fibers);
                fibers = NULL;

                return 1;
            }

            cpl_frameset_insert(set, rimg_frame);

        }

        giraffe_fov_delete(fov);
        fov = NULL;

    }


    /*
     * Cleanup
     */

    giraffe_table_delete(wcalcoeff);

    giraffe_table_delete(slitgeometry);
    giraffe_table_delete(grating);
    giraffe_table_delete(fibers);

    giraffe_rebinning_destroy(rebinning);

    return 0;

}


/*
 * Build table of contents, i.e. the list of available plugins, for
 * this module. This function is exported.
 */

int
cpl_plugin_get_info(cpl_pluginlist* list)
{

    cpl_recipe* recipe = cx_calloc(1, sizeof *recipe);
    cpl_plugin* plugin = &recipe->interface;


    cpl_plugin_init(plugin,
                    CPL_PLUGIN_API,
                    GIRAFFE_BINARY_VERSION,
                    CPL_PLUGIN_TYPE_RECIPE,
                    "giscience",
                    "Process a science observation.",
                    "For detailed information please refer to the "
                    "GIRAFFE pipeline user manual.\nIt is available at "
                    "http://www.eso.org/pipelines.",
                    "Giraffe Pipeline",
                    PACKAGE_BUGREPORT,
                    giraffe_get_license(),
                    giscience_create,
                    giscience_exec,
                    giscience_destroy);

    cpl_pluginlist_append(list, plugin);

    return 0;

}


/* Structure of a lookup table entry for the LUT used to get values for
 * SPEC_RES. */
typedef struct _giraffe_lut_entry {
    const char* expmode;
    double specres;
    double lamrms;  /* given in (px), to convert to (nm) one needs:
                       lamrms * spec_length / 4100 */
    double spec_length;
    int lamnlin;
} giraffe_lut_entry;


#ifndef NDEBUG

/**
 * @internal
 * @brief Check to see if the expmode strings in the lookup table are sorted.
 */
static cpl_boolean _giraffe_lut_is_sorted(const giraffe_lut_entry* lut,
                                          size_t nentries)
{
    size_t i;
    if (nentries < 2) return CPL_TRUE;
    for (i = 0; i < nentries - 1; ++i) {
        if (strcmp(lut[i].expmode, lut[i+1].expmode) >= 0) {
            return CPL_FALSE;
        }
    }
    return CPL_TRUE;
}

#endif /* NDEBUG */


/**
 * @internal
 * @brief Finds an entry for a particular exposure mode in the internal LUT.
 * @param expmode  The mode string to lookup.
 * @returns The lookup table entry corresponding to @c expmode or NULL if one
 *          could not be found.
 */
static const giraffe_lut_entry* _giraffe_find_lut_entry(const char* expmode)
{
    static giraffe_lut_entry lut[] = {
    /* INS_EXP_MODE         lambda      LAMRMS     spec_length        LAMNLIN
     *               R = -------------   (px)         (nm)          line count
     *                   delta(lambda)
     */
        {"H379.",       25000,          0.2250,         20.0,           60},
        {"H379.0",      25000,          0.2250,         20.0,           60},
        {"H395.8",      22000,          0.1913,         21.1,           53},
        {"H412.4",      30000,          0.1326,         22.3,           48},
        {"H429.7",      23000,          0.1471,         23.5,           67},
        {"H447.1",      20000,          0.0906,         24.9,           63},
        {"H447.1A",     20000,          0.0906,         24.9,           63},
        {"H447.1B",     31000,          0.1297,         23.5,           58},
        {"H465.6",      23000,          0.1573,         22.4,           57},
        {"H484.5",      19000,          0.1175,         23.5,           57},
        {"H484.5A",     19000,          0.1175,         23.5,           57},
        {"H484.5B",     33000,          0.0907,         24.6,           61},
        {"H504.8",      22000,          0.1726,         25.2,           56},
        {"H525.8",      17000,          0.1397,         25.8,           49},
        {"H525.8A",     17000,          0.1397,         25.8,           49},
        {"H525.8B",     29000,          0.2014,         26.4,           44},
        {"H548.8",      20000,          0.2389,         26.9,           51},
        {"H572.8",      27000,          0.1844,         27.5,           49},
        {"H599.3",      18000,          0.1683,         28.3,           57},
        {"H627.3",      24000,          0.1268,         29.0,           50},
        {"H651.5",      17000,          0.1185,         30.0,           42},
        {"H651.5A",     17000,          0.1185,         30.0,           42},
        {"H651.5B",     35000,          0.1253,         31.5,           37},
        {"H665.",       17000,          0.2076,         33.0,           45},
        {"H665.0",      17000,          0.2076,         33.0,           45},
        {"H679.7",      19000,          0.1621,         34.5,           51},
        {"H710.5",      25000,          0.1495,         36.0,           48},
        {"H737.",       16000,          0.1456,         37.5,           47},
        {"H737.0",      16000,          0.1456,         37.5,           47},
        {"H737.0A",     16000,          0.1456,         37.5,           47},
        {"H737.0B",     35000,          0.1147,         39.5,           40},
        {"H769.1",      19000,          0.2811,         41.8,           35},
        {"H805.3",      14000,          0.2662,         42.4,           39},
        {"H805.3A",     14000,          0.2662,         42.4,           39},
        {"H805.3B",     25000,          0.2342,         42.9,           28},
        {"H836.6",      16000,          0.2032,         43.3,           21},
        {"H836.6A",     16000,          0.2032,         43.3,           21},
        {"H836.6B",     34000,          0.1073,         43.7,           14},
        {"H875.7",      18000,          0.2026,         44.3,           29},
        {"H920.5",      12000,          0.1568,         44.9,           32},
        {"H920.5A",     12000,          0.1568,         44.9,           32},
        {"H920.5B",     24000,          0.2531,         45.9,           28},
        {"L385.7",      7500,           0.3358,         58.0,           43},
        {"L427.2",      6300,           0.2152,         61.1,           62},
        {"L479.7",      7500,           0.1554,         71.0,           61},
        {"L543.1",      5800,           0.2065,         82.1,           58},
        {"L614.2",      6600,           0.1803,         79.0,           51},
        {"L682.2",      8100,           0.1843,         74.0,           50},
        {"L773.4",      5400,           0.1617,         94.0,           44},
        {"L881.7",      6600,           0.1614,         119.0,          29}
    };
    static const size_t nentries = sizeof(lut) / sizeof(giraffe_lut_entry);
    int low = 0;              /* Bottom of search region. */
    int high = (int)nentries - 1;  /* Top of search region. */

    assert(_giraffe_lut_is_sorted(lut, nentries));
    assert(expmode != NULL);

    /* Perform a binary search for the entry. */
    do {
        int mid = (low + high) >> 1;  /* Find mid point of search range. */
        int result = strcmp(expmode, lut[mid].expmode);
        if (result == 0) {
            return &lut[mid];
        } else if (result < 0) {
            high = mid - 1;
        } else {
            low = mid + 1;
        }
    } while (high >= low);
    return NULL;
}

/**
 * @internal
 * @brief Looks up the value to use for SPEC_RES from an internal LUT.
 * @param expmode  The mode string to lookup.
 * @returns The spectral resolution corresponding to the given mode string.
 */
static double _giraffe_lookup_specres(const char* expmode)
{
    const giraffe_lut_entry* entry = _giraffe_find_lut_entry(expmode);
    if (entry == NULL) return NAN;
    return entry->specres;
}

/**
 * @internal
 * @brief Looks up the LAMRMS value to use from an internal LUT.
 * @param expmode  The mode string to lookup.
 * @returns The LAMRMS in nanometers or NAN if none could be found.
 */
static double _giraffe_lookup_lamrms(const char* expmode)
{
    const giraffe_lut_entry* entry = _giraffe_find_lut_entry(expmode);
    if (entry == NULL) return NAN;
    if (isnan(entry->lamrms) || isnan(entry->spec_length)) return NAN;
    return entry->lamrms * entry->spec_length / 4100.;
}

/**
 * @internal
 * @brief Looks up the LAMNLIN value to use from an internal LUT.
 * @param expmode  The mode string to lookup.
 * @returns The LAMNLIN or -1 if none could be found.
 */
static double _giraffe_lookup_lamnlin(const char* expmode)
{
    const giraffe_lut_entry* entry = _giraffe_find_lut_entry(expmode);
    if (entry == NULL) return -1;
    return entry->lamnlin;
}


/**
 * @internal
 * @brief Calculate the data type to be used for the WAVE column from WCS data.
 * @param crval2  Value given by the CRVAL2 FITS keyword.
 * @param crpix2  Value given by the CRPIX2 FITS keyword.
 * @param crdelt2 Value given by the CDELT2 FITS keyword.
 * @param naxis2  Value given by the NAXIS2 FITS keyword.
 * @return Either CPL_TYPE_DOUBLE or CPL_TYPE_FLOAT, depending on the required
 *         precision derived from the WCS keywords.
 */
static cpl_type _giraffe_calc_wave_type(double crval2, double crpix2,
                                        double cdelt2, cpl_size naxis2)
{
    static const double errfrac = 0.02;
    static const double single_precision_digits = 7;
    double lo = (1.0 - crpix2) * cdelt2;
    double hi = ((double)naxis2 - crpix2) * cdelt2;
    double maxwave = crval2 + (hi > lo ? hi : lo);
    double binfrac = (maxwave != 0.0) ? fabs(cdelt2 / maxwave) : 0.0;
    if (binfrac * errfrac < pow(10, -single_precision_digits)) {
        return CPL_TYPE_DOUBLE;
    } else {
        return CPL_TYPE_FLOAT;
    }
}


/**
 * @internal
 * @brief Check if any ancillary spectra are available.
 * @param filename  The name of the file from which @c fibertable is loaded.
 * @param fibertable  The fiber table object loaded from @c filename.
 * @returns CPL_TRUE if there is at least one ancillary spectrum. CPL_FALSE is
 *          returned if no ancillary data was found or if an error occurred. One
 *          should check the CPL error state to confirm if an error occurred.
 */
static cpl_boolean _giraffe_ancillary_data_available(const char* filename,
                                                     const GiTable* fibertable)
{
    cpl_table* tbl = giraffe_table_get(fibertable);
    cpl_size i;
    const char** spectypes = NULL;

    assert(filename != NULL);

    cpl_error_ensure(tbl != NULL, cpl_error_get_code(), return CPL_FALSE,
                     "The fiber table is not available for '%s'.", filename);

    spectypes = cpl_table_get_data_string_const(tbl, GIALIAS_COLUMN_TYPE);
    cpl_error_ensure(spectypes != NULL, cpl_error_get_code(), return CPL_FALSE,
                     "Could not fetch the '%s' column from the fiber setup"
                     " table in '%s'.", GIALIAS_COLUMN_TYPE, filename);
    /*
     * Step through the table and check for fiber types other than Medusa
     * fibers. Also the simultaneous calibration fibers are not considered
     * as ancillary spectra and are ignored.
     */
    for (i = 0; i < cpl_table_get_nrow(tbl); ++i) {
        if ((spectypes[i][0] != '\0') && (strcmp(spectypes[i], "M") != 0)) {
            return CPL_TRUE;
        }
    }
    return CPL_FALSE;
}


/**
 * @internal
 * @brief Generate the stripped out ancillary spectrum product.
 *
 * All spectra that are not marked as science are saved to the ancillary file.
 * Any science spectra are removed. The newly created file is added to the set
 * of frames.
 *
 * @param allframes The set of all frames to which the newly created ancillary
 *                  file is added.
 * @param outputfile The name of the output file to create.
 * @param infilename The filename of the file from which the @c fluximage and
 *                   @c fibertable values are loaded.
 * @param fluximage The spectrum image loaded from the @c infilename file.
 * @param fibertable The fiber table loaded from the @c infilename file.
 * @returns CPL_ERROR_NONE if the ancillary file was created successfully and
 *          an error code otherwise. The error is also added to the CPL error
 *          state.
 */
static cpl_error_code _giraffe_make_ancillary_file(cpl_frameset* allframes,
                                                   const char* outputfile,
                                                   const char* infilename,
                                                   const GiImage* fluximage,
                                                   const GiTable* fibertable)
{
    cpl_error_code error = CPL_ERROR_NONE;
    cxint retcode;
    cpl_frame* frame = NULL;
    cpl_image* srcimg = giraffe_image_get(fluximage);
    GiImage* image = NULL;
    GiTable* table = giraffe_table_duplicate(fibertable);
    cpl_image* img = NULL;
    cpl_table* tbl = giraffe_table_get(table);
    cpl_image* subimg = NULL;
    cpl_propertylist* comments = NULL;
    cpl_size ny, i;
    int* indices = NULL;
    const char** spectypes = NULL;

    assert(allframes != NULL);
    assert(outputfile != NULL);
    assert(infilename != NULL);

    cpl_error_ensure(srcimg != NULL && table != NULL && tbl != NULL,
                     cpl_error_get_code(), goto cleanup,
                     "The image or table are not available for '%s'.",
                     infilename);

    /* Setup a new frame for the output file. */
    frame = cpl_frame_new();
    error |= cpl_frame_set_filename(frame, outputfile);
    error |= cpl_frame_set_tag(frame, GIALIAS_ASSO_PROCATG_VALUE);
    error |= cpl_frame_set_type(frame, CPL_FRAME_TYPE_IMAGE);
    error |= cpl_frame_set_group(frame, CPL_FRAME_GROUP_PRODUCT);
    error |= cpl_frame_set_level(frame, CPL_FRAME_LEVEL_FINAL);
    cpl_error_ensure(! error, cpl_error_get_code(), goto cleanup,
                     "Failed to setup a new output frame for '%s'.",
                     outputfile);

    /* First step to filter out the science entries in the image and fiber setup
     * table is to go through the table entries, mark the entries to remove, and
     * delete the ones that are marked. i.e. unselect everything, select all
     * non-science spectra, ignoring the simultaneous calibration spectra,
     * invert the selection and remove everything that remains marked as
     * selected. */
    error |= cpl_table_unselect_all(tbl);
    cpl_error_ensure(! error, cpl_error_get_code(), goto cleanup,
                     "Failed to unselect all entries in fiber setup table from"
                     " '%s'.", infilename);
    cpl_table_or_selected_string(tbl, GIALIAS_COLUMN_TYPE, CPL_NOT_EQUAL_TO,
                                 "M");
    cpl_table_and_selected_int(tbl, GIALIAS_COLUMN_RP, CPL_NOT_EQUAL_TO, -1);
    cpl_table_not_selected(tbl);
    error |= cpl_table_erase_selected(tbl);
    cpl_error_ensure(! error, cpl_error_get_code(), goto cleanup,
                     "Failed to erase selected entries in fiber setup table"
                     " from '%s'.", infilename);

    /* Create a new output image which is wide enough to store the data with the
     * stripped out spectra removed. */
    ny = cpl_image_get_size_y(srcimg);
    image = giraffe_image_create(cpl_image_get_type(srcimg),
                                 cpl_table_get_nrow(tbl), ny);
    img = giraffe_image_get(image);
    retcode = giraffe_image_set_properties(
                                image, giraffe_image_get_properties(fluximage));
    cpl_error_ensure(image != NULL && img != NULL && retcode == 0,
                     cpl_error_get_code(), goto cleanup,
                     "Failed to create image for output file '%s'.",
                     outputfile);

    error |= cpl_propertylist_update_string(giraffe_image_get_properties(image),
                                            GIALIAS_PROCATG,
                                            GIALIAS_ASSO_PROCATG_VALUE);
    cpl_error_ensure(! error, cpl_error_get_code(), goto cleanup,
                     "Could not update keyword '%s' for output file '%s'.",
                     GIALIAS_PROCATG, outputfile);

    /* Strip out extra comments which will be added by CPL anyway. */
    if (cpl_propertylist_has(giraffe_image_get_properties(image), "COMMENT")) {
        cpl_size ic;
        const char* comments_to_remove[] = {
            "  FITS (Flexible Image Transport System) format is defined in"
              " 'Astronomy",
            "  and Astrophysics', volume 376, page 359; bibcode: 2001A&A..."
              "376..359H",
            NULL
        };
        cpl_propertylist* props = giraffe_image_get_properties(image);
        comments = cpl_propertylist_new();
        error |= cpl_propertylist_copy_property_regexp(comments, props,
                                                       "^COMMENT$", 0);
        cpl_propertylist_erase_regexp(props, "^COMMENT$", 0);
        for (ic = 0; ic < cpl_propertylist_get_size(comments); ++ic) {
            const char** cmnt_str;
            cpl_property* p = cpl_propertylist_get(comments, ic);
            for (cmnt_str = comments_to_remove; *cmnt_str != NULL; ++cmnt_str) {
                if (strcmp(cpl_property_get_string(p), *cmnt_str) == 0) {
                    goto dont_add_comment;
                }
            }
            /* Add back comments that should not be removed. */
            error |= cpl_propertylist_append_property(props, p);
        dont_add_comment:
            /* Land up here if the comment was found in the comments_to_remove
             * list of strings. */
            ;
        }
        cpl_error_ensure(! error, cpl_error_get_code(), goto cleanup,
                         "Failed to cleanup comments in primary HDU for '%s'.",
                         outputfile);
        cpl_propertylist_delete(comments);
        comments = NULL;
    }

    /* We now have to relabel the index numbers in the fiber setup table and
     * copy corresponding image columns to the new image. */
    indices = cpl_table_get_data_int(tbl, GIALIAS_COLUMN_INDEX);
    cpl_error_ensure(indices != NULL, cpl_error_get_code(), goto cleanup,
                     "Could not fetch the '%s' column from the fiber setup"
                     " table in '%s'.", GIALIAS_COLUMN_INDEX, infilename);
    for (i = 0; i < cpl_table_get_nrow(tbl); ++i) {
        cpl_size oldindex = indices[i];
        cpl_size newindex = i+1;
        indices[i] = newindex;
        subimg = cpl_image_extract(srcimg, oldindex, 1, oldindex, ny);
        cpl_error_ensure(subimg != NULL, cpl_error_get_code(), goto cleanup,
                         "Could not extract sub image from '%s' at column %"
                         CPL_SIZE_FORMAT".", infilename, oldindex);
        error |= cpl_image_copy(img, subimg, newindex, 1);
        cpl_image_delete(subimg);
        subimg = NULL;
        cpl_error_ensure(! error, cpl_error_get_code(), goto cleanup,
                         "Could write sub image from '%s' at column %"
                         CPL_SIZE_FORMAT" to new image in '%s' at column %"
                         CPL_SIZE_FORMAT".", infilename, oldindex, outputfile,
                         newindex);
    }

    /* Now write the actual FITS file. */
    retcode = giraffe_image_save(image, outputfile);
    cpl_error_ensure(retcode == 0, cpl_error_get_code(), goto cleanup,
                     "Failed to write image to file '%s'.", outputfile);
    retcode = giraffe_fiberlist_attach(frame, table);
    cpl_error_ensure(retcode == 0, cpl_error_get_code(), goto cleanup,
                     "Failed to attach the fiber setup table to file '%s'.",
                     outputfile);

    /* Add the new frame to the output frame set for the ancillary file.
     * Note: this should be the last step so that we do not delete this frame
     * anymore once in the frame set. */
    error |= cpl_frameset_insert(allframes, frame);
    cpl_error_ensure(! error, cpl_error_get_code(), goto cleanup,
                     "Could not add a new frame to the frame list for '%s'.",
                     outputfile);

    giraffe_image_delete(image);
    giraffe_table_delete(table);
    return CPL_ERROR_NONE;

cleanup:
    /* Error handling. Note: NULL pointer checks done by delete functions. */
    cpl_image_delete(subimg);
    giraffe_image_delete(image);
    giraffe_table_delete(table);
    cpl_frame_delete(frame);
    cpl_propertylist_delete(comments);
    return cpl_error_get_code();
}


/**
 * @internal
 * @brief Prepares a format string to use for the SDP spectrum filenames.
 * @param maxfiles  The maximum number of files that will be produced.
 * @return a format string that must be cleaned up with @c cpl_free.
 */
static char* _giraffe_calc_format_string(cpl_size maxfiles)
{
    double ndigits = 1.0;
    if (maxfiles > 1) {
        /* Figure out how many digits must be printed to support a index number
         * as large as 'maxfiles'. */
        ndigits = ceil(log10((double)maxfiles + 1.0));
    }
    return cpl_sprintf("science_spectrum_%%0%.0f"CPL_SIZE_FORMAT".fits",
                       ndigits);
}


/**
 * @internal
 * @brief Converts output spectra to SDP format.
 * @param flux_filename The name of the file containing the image with
 *                      spectrum flux values.
 * @param err_filename The name of the file containing the statistical error
 *                     values for the spectrum flux's.
 * @param nassoc_keys The number of extra dummy association keys to generate in
 *                    the 1D SDP spectra.
 * @param allframes The set of all input frames to which we add any newly
 *                  created 1D SDP or ancillary spectrum files.
 * @param parlist The list of parameters passed to the recipe.
 * @param recipe_id The name of the recipe being run.
 * @returns 0 on success and a non-zero value if an error occurred. The CPL
 *          error state will be set with appropriate error codes and messages.
 */
static cxint _giraffe_make_sdp_spectra(const cxchar* flux_filename,
                                       const cxchar* err_filename,
                                       cxint nassoc_keys,
                                       cpl_frameset* allframes,
                                       const cpl_parameterlist* parlist,
                                       const cxchar* recipe_id)
{
    cxint result_code = 1;
    cxint errorcode;
    cpl_error_code error = CPL_ERROR_NONE;
    cpl_errorstate prestate;
    const char* ancillary_filename = "science_ancillary.fits";
    GiImage* fluximage = giraffe_image_new(CPL_TYPE_DOUBLE);
    GiImage* errimage = giraffe_image_new(CPL_TYPE_DOUBLE);
    GiTable* fibertable = NULL;
    const cxchar* fibertable_name = NULL;
    irplib_sdp_spectrum* spectrum = NULL;
    cpl_propertylist* extrakeys = cpl_propertylist_new();
    cpl_propertylist* tablekeys = cpl_propertylist_new();
    cpl_propertylist* props;
    char* pipe_id = cpl_sprintf("%s/%s", PACKAGE, VERSION);
    const char* dict_id = PRODUCT_DID;
    const cpl_frame* inherit = NULL;
    cpl_frameset* usedframes = NULL;
    cpl_frameset* rawframes = NULL;
    cpl_frameset_iterator* iterator = NULL;
    cpl_size nx, ny, i, filecount;
    double exptime = NAN;
    double mjdobs = NAN;
    double mjdend = NAN;
    double wavelmin = NAN;
    double wavelmax = NAN;
    double specbin = NAN;
    double crpix2 = NAN;
    double crval2 = NAN;
    double cdelt2 = NAN;
    const char* cunit2 = NULL;
    double specres;
    const char* expmode = NULL;
    char strbuf[64];
    const int* indices = NULL;
    const int* fps = NULL;
    const char** objects = NULL;
    const char** spectypes = NULL;
    const double* ras = NULL;
    const double* decs = NULL;
    const double* gcorr = NULL;
    const double* hcorr = NULL;
    const double* bcorr = NULL;
    char* formatstr = NULL;
    char* filename = NULL;
    cpl_type wavecoltype;
    cpl_array* refwavearray = NULL;
    cpl_array* array = NULL;
    float* data_float = NULL;
    double* data_double = NULL;
    cpl_vector* fluximgcol = NULL;
    cpl_vector* errimgcol = NULL;
    cpl_boolean got_ancillary_data = CPL_FALSE;
    cpl_size assoc_key_offset = 1;
    int lamnlin = -1;
    double lamrms = NAN;
    double specerr = NAN;
    double specsye = NAN;

    cpl_error_ensure(flux_filename != NULL && err_filename != NULL
                     && allframes != NULL && parlist != NULL
                     && recipe_id != NULL, CPL_ERROR_NULL_INPUT, goto cleanup,
                     "NULL input parameters.");

    error |= cpl_propertylist_append_string(extrakeys, GIALIAS_PROCATG,
                                            GIALIAS_PROCATG_RBNSPEC_IDP);
    error |= cpl_propertylist_set_comment(extrakeys, GIALIAS_PROCATG,
                                          GIALIAS_PROCATG_COMMENT);
    cpl_error_ensure(! error, cpl_error_get_code(), goto cleanup,
                     "Could not set keyword '%s'.", GIALIAS_PROCATG);

    /* Load the input flux and error data, including FITS header keywords. */
    errorcode = giraffe_image_load(fluximage, flux_filename, 0);
    cpl_error_ensure(errorcode == 0, cpl_error_get_code(), goto cleanup,
                     "Could not load image data in primary HDU from '%s'.",
                     flux_filename);
    errorcode = giraffe_image_load(errimage, err_filename, 0);
    cpl_error_ensure(errorcode == 0, cpl_error_get_code(), goto cleanup,
                     "Could not load image data in primary HDU from '%s'.",
                     err_filename);

    giraffe_error_push();
    fibertable = giraffe_fiberlist_load(flux_filename, 1, GIALIAS_FIBER_SETUP);
    if (fibertable == NULL) {
        fibertable = giraffe_fiberlist_load(err_filename, 1,
                                            GIALIAS_FIBER_SETUP);
        fibertable_name = err_filename;
    } else {
        fibertable_name = flux_filename;
    }
    cpl_error_ensure(fibertable != NULL, CPL_ERROR_DATA_NOT_FOUND, goto cleanup,
                     "Could not load the %s table from either '%s' or '%s'.",
                     GIALIAS_FIBER_SETUP, flux_filename, err_filename);
    giraffe_error_pop();

    /* Check that the image sizes are the same. */
    nx = cpl_image_get_size_x(giraffe_image_get(fluximage));
    ny = cpl_image_get_size_y(giraffe_image_get(fluximage));
    cpl_error_ensure(cpl_image_get_size_x(giraffe_image_get(errimage)) == nx
                     && cpl_image_get_size_y(giraffe_image_get(errimage)) == ny,
                     CPL_ERROR_INCOMPATIBLE_INPUT, goto cleanup,
                     "The images in files '%s' and '%s' are not the same size.",
                     flux_filename, err_filename);

    /* Construct the used frame list from the existing list of all frames.
     * The frames to be included as the used frames are RAW and CALIB. */
    usedframes = cpl_frameset_new();
    rawframes = cpl_frameset_new();
    iterator = cpl_frameset_iterator_new(allframes);
    do {
        const cpl_frame* frame = cpl_frameset_iterator_get_const(iterator);
        if (frame != NULL) {
            switch (cpl_frame_get_group(frame)) {
            case CPL_FRAME_GROUP_RAW:
                /* Mark the first RAW frame from which to inherit keywords. */
                if (inherit == NULL) inherit = frame;
                error |= cpl_frameset_insert(rawframes,
                                             cpl_frame_duplicate(frame));
                error |= cpl_frameset_insert(usedframes,
                                             cpl_frame_duplicate(frame));
                break;
            case CPL_FRAME_GROUP_CALIB:
                error |= cpl_frameset_insert(usedframes,
                                             cpl_frame_duplicate(frame));
                break;
            default:  /* Ignore all other groups */
                break;
            }
        }
        prestate = cpl_errorstate_get();
        error |= cpl_frameset_iterator_advance(iterator, 1);
        if (error == CPL_ERROR_ACCESS_OUT_OF_RANGE) {
            cpl_errorstate_set(prestate);
            break;
        } else if (error != CPL_ERROR_NONE) {
            goto cleanup;
        }
    } while (1);

    cpl_error_ensure(inherit != NULL, CPL_ERROR_DATA_NOT_FOUND, goto cleanup,
                     "No raw input frames found.");

    /* Fetch the EXPTIME and MJD-OBS keywords from the product file. */
    props = giraffe_image_get_properties(fluximage);
    prestate = cpl_errorstate_get();
    exptime = cpl_propertylist_get_double(props, GIALIAS_EXPTIME);
    cpl_error_ensure(cpl_errorstate_is_equal(prestate), cpl_error_get_code(),
                     goto cleanup, "Could not find keyword '%s' in '%s'.",
                     GIALIAS_EXPTIME, flux_filename);
    mjdobs = cpl_propertylist_get_double(props, GIALIAS_MJDOBS);
    cpl_error_ensure(cpl_errorstate_is_equal(prestate), cpl_error_get_code(),
                     goto cleanup, "Could not find keyword '%s' in '%s'.",
                     GIALIAS_MJDOBS, flux_filename);

    mjdend = mjdobs + exptime / 86400.;

    /* Calculate the min/max wavelength values. */
    crpix2 = cpl_propertylist_get_double(props, GIALIAS_CRPIX2);
    cpl_error_ensure(cpl_errorstate_is_equal(prestate), cpl_error_get_code(),
                     goto cleanup, "Could not find keyword '%s' in '%s'.",
                     GIALIAS_CRPIX2, flux_filename);
    crval2 = cpl_propertylist_get_double(props, GIALIAS_CRVAL2);
    cpl_error_ensure(cpl_errorstate_is_equal(prestate), cpl_error_get_code(),
                     goto cleanup, "Could not find keyword '%s' in '%s'.",
                     GIALIAS_CRVAL2, flux_filename);
    cdelt2 = cpl_propertylist_get_double(props, GIALIAS_CDELT2);
    cpl_error_ensure(cpl_errorstate_is_equal(prestate), cpl_error_get_code(),
                     goto cleanup, "Could not find keyword '%s' in '%s'.",
                     GIALIAS_CDELT2, flux_filename);
    cunit2 = cpl_propertylist_get_string(props, GIALIAS_CUNIT2);
    cpl_error_ensure(cunit2 != NULL, cpl_error_get_code(),
                     goto cleanup, "Could not find keyword '%s' in '%s'.",
                     GIALIAS_CUNIT2, flux_filename);

    if (strcmp(cunit2, "nm") == 0) {
        wavelmin = (1.0 - crpix2) * cdelt2 + crval2;
        wavelmax = ((double)ny - crpix2) * cdelt2 + crval2;
        if (wavelmax < wavelmin) {
            double tmp = wavelmin;
            wavelmin = wavelmax;
            wavelmax = tmp;
        }
        specbin = fabs(cdelt2);
    } else {
        cpl_msg_warning(cpl_func, "Do not know how to handle keyword %s = '%s'."
                        " Will not set WAVELMIN, WAVELMAX or SPEC_BIN.",
                        GIALIAS_CUNIT2, cunit2);
    }

    if (cpl_propertylist_has(props, GIALIAS_SETUPNAME)) {
        expmode = cpl_propertylist_get_string(props, GIALIAS_SETUPNAME);
        cpl_error_ensure(expmode != NULL, cpl_error_get_code(), goto cleanup,
                         "Could not fetch the keyword '%s' from '%s'.",
                         GIALIAS_SETUPNAME, flux_filename);
    } else if (cpl_propertylist_has(props, GIALIAS_GRATNAME)) {
        const char* name = cpl_propertylist_get_string(props, GIALIAS_GRATNAME);
        cpl_error_ensure(name != NULL, cpl_error_get_code(), goto cleanup,
                         "Could not fetch the keyword '%s' from '%s'.",
                         GIALIAS_GRATNAME, flux_filename);
        double wlen = cpl_propertylist_get_double(props, GIALIAS_GRATWLEN);
        cpl_error_ensure(cpl_errorstate_is_equal(prestate),
                         cpl_error_get_code(), goto cleanup,
                         "Could not find keyword '%s' in '%s'.",
                         GIALIAS_GRATWLEN, flux_filename);
        strbuf[0] = name[0];
        char* numstr = cpl_sprintf("%.1f", wlen);
        strncpy(strbuf+1, numstr, sizeof(strbuf)-1);
        cpl_free(numstr);
        strbuf[sizeof(strbuf)-1] = '\0'; /* Ensure we have a NULL terminator. */
        expmode = strbuf;
    } else {
        cpl_error_set_message(cpl_func, CPL_ERROR_DATA_NOT_FOUND,
                    "Neither '%s' nor '%s' and '%s' keywords were found in the"
                    " file '%s'.", GIALIAS_SETUPNAME, GIALIAS_GRATNAME,
                    GIALIAS_GRATWLEN, flux_filename);
        goto cleanup;
    }

    specres = _giraffe_lookup_specres(expmode);
    if (isnan(specres)) {
        cpl_error_set_message(cpl_func, CPL_ERROR_ILLEGAL_INPUT,
                    "The exposure mode '%s' is invalid or an unknown value."
                    " Could not lookup the spectral resolution for 'SPEC_RES'.",
                    expmode);
        goto cleanup;
    }

    /* Add the FILTER keyword as OFILTER to the extra keywords list if it
     * exists. The FILTER keyword itself will be deleted. */
    if (cpl_propertylist_has(props, "FILTER")) {
        prestate = cpl_errorstate_get();
        cpl_propertylist_copy_property(extrakeys, props, "FILTER");
        cpl_property* prop = cpl_propertylist_get_property(extrakeys, "FILTER");
        cpl_property_set_name(prop, "OFILTER");
        cpl_error_ensure(cpl_errorstate_is_equal(prestate),
                         cpl_error_get_code(), goto cleanup,
                         "Could not rename the 'FILTER' keyword.");
    }

    /* Write the ancillary data file if any ancillary data is available. */
    prestate = cpl_errorstate_get();
    got_ancillary_data = _giraffe_ancillary_data_available(flux_filename,
                                                           fibertable);
    if (! cpl_errorstate_is_equal(prestate)) goto cleanup;
    if (got_ancillary_data) {
        error = _giraffe_make_ancillary_file(allframes, ancillary_filename,
                                             flux_filename, fluximage,
                                             fibertable);
        cpl_error_ensure(! error, cpl_error_get_code(), goto cleanup,
                         "Failed to write the ancillary file '%s'.",
                         ancillary_filename);
    }

    /* Create a new spectrum object and setup header keywords. */
    spectrum = irplib_sdp_spectrum_new();
    error = CPL_ERROR_NONE;
    error |= irplib_sdp_spectrum_set_origin(spectrum, GIALIAS_ORIGIN_VALUE);
    error |= irplib_sdp_spectrum_set_prodlvl(spectrum, GIALIAS_PRODLVL_VALUE);
    error |= irplib_sdp_spectrum_copy_dispelem(spectrum,
                                               props, GIALIAS_GRATNAME);
    error |= irplib_sdp_spectrum_set_specsys(spectrum, GIALIAS_SPECSYS_VALUE);
    error |= irplib_sdp_spectrum_set_extobj(spectrum, GIALIAS_EXT_OBJ_VALUE);
    /* The OBJECT, RA and DEC keywords we fill now with dummy values to maintain
     * the order of keywords. The actual values are set later. */
    error |= irplib_sdp_spectrum_set_object(spectrum, "");
    error |= irplib_sdp_spectrum_set_ra(spectrum, 0.0);
    error |= irplib_sdp_spectrum_set_dec(spectrum, 0.0);
    error |= irplib_sdp_spectrum_copy_exptime(spectrum, props, GIALIAS_EXPTIME);
    error |= irplib_sdp_spectrum_copy_texptime(spectrum,
                                               props, GIALIAS_EXPTIME);
    error |= irplib_sdp_spectrum_copy_mjdobs(spectrum, props, GIALIAS_MJDOBS);
    error |= irplib_sdp_spectrum_set_mjdend(spectrum, mjdend);
    if (cpl_propertylist_has(props, GIALIAS_TIMESYS)) {
      error |= irplib_sdp_spectrum_copy_timesys(spectrum,
                                                props, GIALIAS_TIMESYS);
    }
    error |= irplib_sdp_spectrum_copy_progid(spectrum, props, GIALIAS_PROGID);
    error |= irplib_sdp_spectrum_copy_obid(spectrum, 1, props, GIALIAS_OBSID);
    error |= irplib_sdp_spectrum_set_mepoch(spectrum, GIALIAS_M_EPOCH_VALUE);
    error |= irplib_sdp_spectrum_append_prov(spectrum, 1, rawframes);
    error |= irplib_sdp_spectrum_copy_procsoft(spectrum,
                                               props, GIALIAS_PROPIPEID);
    error |= irplib_sdp_spectrum_copy_obstech(spectrum, props, GIALIAS_PROTECH);
    error |= irplib_sdp_spectrum_set_prodcatg(spectrum, GIALIAS_PRODCATG_VALUE);
    error |= irplib_sdp_spectrum_set_fluxcal(spectrum, GIALIAS_FLUXCAL_VALUE);
    error |= irplib_sdp_spectrum_set_contnorm(spectrum, GIALIAS_CONTNORM_VALUE);
    /* Set dummy values for WAVELMIN, WAVELMAX and SPEC_BIN to keep the order
     * of the keywords. Will fill these in later with actual Heliocentric
     * corrected values. */
    error |= irplib_sdp_spectrum_set_wavelmin(spectrum, 0.0);
    error |= irplib_sdp_spectrum_set_wavelmax(spectrum, 0.0);
    error |= irplib_sdp_spectrum_set_specbin(spectrum, 0.0);
    error |= irplib_sdp_spectrum_set_totflux(spectrum, GIALIAS_TOTFLUX_VALUE);
    error |= irplib_sdp_spectrum_set_fluxerr(spectrum, GIALIAS_FLUXERR_VALUE);
    error |= irplib_sdp_spectrum_set_ncombine(spectrum,
                                              cpl_frameset_get_size(rawframes));
    error |= irplib_sdp_spectrum_set_referenc(spectrum, GIALIAS_REFERENC);

    /* Set dummy value for SNR to maintain keyword ordering. Filled later. */
    error |= irplib_sdp_spectrum_set_snr(spectrum, 0.0);

    /* Copy LAMNLIN if available from flux image else try look it up. */
    if (cpl_propertylist_has(props, GIALIAS_LAMNLIN)) {
        error |= irplib_sdp_spectrum_copy_lamnlin(spectrum, props,
                                                  GIALIAS_LAMNLIN);
        lamnlin = irplib_sdp_spectrum_get_lamnlin(spectrum);
    } else {
        lamnlin = _giraffe_lookup_lamnlin(expmode);
        if (lamnlin != -1) {
            error |= irplib_sdp_spectrum_set_lamnlin(spectrum, lamnlin);
        }
    }

    /* Copy LAMRMS if available from flux image else try look it up.
     * Note: we are going to have to correct this value later and fill it in
     * again for each spectrum. */
    if (cpl_propertylist_has(props, GIALIAS_LAMRMS)) {
        error |= irplib_sdp_spectrum_copy_lamrms(spectrum, props,
                                                 GIALIAS_LAMRMS);
        lamrms = irplib_sdp_spectrum_get_lamrms(spectrum);
    } else {
        lamrms = _giraffe_lookup_lamrms(expmode);
        if (! isnan(lamrms)) {
            error |= irplib_sdp_spectrum_set_lamrms(spectrum, lamrms);
        }
    }

    /* Copy SPEC_ERR if available from the flux image, else estimate it as:
     *   if CRDER1 is available then
     *     SPEC_ERR = CRDER1 / sqrt(LAMNLIN)
     *   else
     *     SPEC_ERR = LAMRMS / sqrt(LAMNLIN)
     *   end
     *
     * Note: we are going to have to correct this value later and fill it in
     * again for each spectrum.
     */
    if (cpl_propertylist_has(props, GIALIAS_SPEC_ERR)) {
        error |= irplib_sdp_spectrum_copy_specerr(spectrum, props,
                                                  GIALIAS_SPEC_ERR);
        specerr = irplib_sdp_spectrum_get_specerr(spectrum);
    } else if (lamnlin > 0) {
        if (cpl_propertylist_has(props, GIALIAS_CRDER1)) {
            prestate = cpl_errorstate_get();
            double crder1 = cpl_propertylist_get_double(props, GIALIAS_CRDER1);
            if (cpl_errorstate_is_equal(prestate) && crder1 > 0) {
                specerr = crder1 / sqrt(lamnlin);
            } else {
                error = cpl_error_get_code();
            }
        } else if (! isnan(lamrms)) {
            specerr = lamrms / sqrt(lamnlin);
        }
        if (! isnan(specerr)) {
            error |= irplib_sdp_spectrum_set_specerr(spectrum, specerr);
        }
    }

    /* Copy SPEC_SYE if available from the flux image, else estimate it as:
     *   0.002 nm
     */
    if (cpl_propertylist_has(props, GIALIAS_SPEC_SYE)) {
        error |= irplib_sdp_spectrum_copy_specsye(spectrum, props,
                                                  GIALIAS_SPEC_SYE);
        specsye = irplib_sdp_spectrum_get_specsye(spectrum);
    } else {
        /* Don't set the local specsye variable so that it does not get
         * corrected later. We want it to always be 0.002 nm. Just set the
         * keyword in the spectrum object directly. */
        error |= irplib_sdp_spectrum_set_specsye(spectrum, 0.002);
    }

    error |= irplib_sdp_spectrum_set_specres(spectrum, specres);
    error |= irplib_sdp_spectrum_copy_gain(spectrum, props, GIALIAS_CONAD);
    error |= irplib_sdp_spectrum_copy_detron(spectrum, props, GIALIAS_RON);
    if (got_ancillary_data) {
        error |= irplib_sdp_spectrum_set_asson(spectrum, 1, ancillary_filename);
        error |= irplib_sdp_spectrum_set_assoc(spectrum, 1,
                                               GIALIAS_ASSOC_VALUE);
        error |= irplib_sdp_spectrum_set_assom(spectrum, 1, "");
        assoc_key_offset = 2;
    }
    for (i = assoc_key_offset; i < nassoc_keys + assoc_key_offset; ++i) {
        /* Add extra dummy association keywords if requested. */
        error |= irplib_sdp_spectrum_set_asson(spectrum, i, "");
        error |= irplib_sdp_spectrum_set_assoc(spectrum, i, "");
        error |= irplib_sdp_spectrum_set_assom(spectrum, i, "");
    }

    error |= irplib_sdp_spectrum_set_voclass(spectrum, GIALIAS_VOCLASS_VALUE);
    error |= irplib_sdp_spectrum_set_vopub(spectrum, GIALIAS_VOPUB_VALUE);
    error |= irplib_sdp_spectrum_set_title(spectrum, ""); /* Set dummy value */
    error |= cpl_propertylist_append_double(tablekeys, GIALIAS_APERTURE,
                                            GIALIAS_APERTURE_VALUE);
    error |= cpl_propertylist_set_comment(tablekeys, GIALIAS_APERTURE,
                                          GIALIAS_APERTURE_COMMENT);
    /*
     * Normally: telapse = (mjdend - mjdobs) * 86400.
     * However, doing this calculation directly leads to rounding errors that
     * can cause the invalid condition TELAPSE < EXPTIME. Since we have:
     *   mjdend = mjdobs + exptime / 86400.
     * we can simplify the above to be: telapse = exptime
     * which will always satisfy the condition TELAPSE >= EXPTIME.
     */
    error |= irplib_sdp_spectrum_set_telapse(spectrum, exptime);
    error |= irplib_sdp_spectrum_set_tmid(spectrum, (mjdobs + mjdend) * 0.5);
    /* Set dummy values for the SPEC_VAL, SPEC_BW, TDMIN and TDMAX values to
     * keep the order of the keywords on the header. Will fill these in later
     * with Heliocentric corrected values. */
    error |= irplib_sdp_spectrum_set_specval(spectrum, 0.0);
    error |= irplib_sdp_spectrum_set_specbw(spectrum, 0.0);
    error |= irplib_sdp_spectrum_set_nelem(spectrum, ny);
    error |= irplib_sdp_spectrum_set_tdmin(spectrum, 0.0);
    error |= irplib_sdp_spectrum_set_tdmax(spectrum, 0.0);

    /* Add the keyword FPS */
    error |= cpl_propertylist_append_int(extrakeys, GIALIAS_FPS, -1);
    error |= cpl_propertylist_set_comment(extrakeys, GIALIAS_FPS,
                                          GIALIAS_FPS_COMMENT);

    /* Add dummy [G,H,B]CORR keywords to be updated from the fiber table. */
    error |= cpl_propertylist_append_double(extrakeys, GIALIAS_GEOCORR, NAN);
    error |= cpl_propertylist_set_comment(extrakeys, GIALIAS_GEOCORR,
                                          GIALIAS_GEOCORR_COMMENT);
    error |= cpl_propertylist_append_double(extrakeys, GIALIAS_HELICORR, NAN);
    error |= cpl_propertylist_set_comment(extrakeys, GIALIAS_HELICORR,
                                          GIALIAS_HELICORR_COMMENT);
    error |= cpl_propertylist_append_double(extrakeys, GIALIAS_BARYCORR, NAN);
    error |= cpl_propertylist_set_comment(extrakeys, GIALIAS_BARYCORR,
                                          GIALIAS_BARYCORR_COMMENT);

    cpl_error_ensure(! error, cpl_error_get_code(), goto cleanup,
                     "Could not setup the common SDP spectrum keywords.");

    /* Figure out the data type required for the WAVE column to preserve the
     * precision of the data values. */
    wavecoltype = _giraffe_calc_wave_type(crval2, crpix2, cdelt2, ny);

    /* Calculate the reference wavelength values (before corrections). */
    refwavearray = cpl_array_new(ny, CPL_TYPE_DOUBLE);
    data_double = cpl_array_get_data_double(refwavearray);
    assert(data_double != NULL);
    for (i = 1; i <= ny; ++i) {
        data_double[i-1] = (i-crpix2) * cdelt2 + crval2;
    }
    data_double = NULL;

    /* Try setup the SDP table columns. */
    error |= irplib_sdp_spectrum_add_column(
                  spectrum, GIALIAS_COLUMN_WAVE, wavecoltype,
                  GIALIAS_COLUMN_WAVE_UNIT, NULL, GIALIAS_COLUMN_WAVE_TUTYP,
                  GIALIAS_COLUMN_WAVE_TUCD, NULL);

    /* Replace default keyword comment of the just created column */
    error |= irplib_sdp_spectrum_replace_column_comment(
                  spectrum, GIALIAS_COLUMN_WAVE, "TUCD", "Air wavelength");

    error |= irplib_sdp_spectrum_set_column_tcomm(
                  spectrum, GIALIAS_COLUMN_WAVE, GIALIAS_COLUMN_WAVE_TCOMM);
    error |= irplib_sdp_spectrum_add_column(
                  spectrum, GIALIAS_COLUMN_FLUX_REDUCED, CPL_TYPE_DOUBLE,
                  GIALIAS_COLUMN_FLUX_REDUCED_UNIT, NULL,
                  GIALIAS_COLUMN_FLUX_REDUCED_TUTYP,
                  GIALIAS_COLUMN_FLUX_REDUCED_TUCD, NULL);
    error |= irplib_sdp_spectrum_set_column_tcomm(
                  spectrum, GIALIAS_COLUMN_FLUX_REDUCED, "");
    error |= irplib_sdp_spectrum_add_column(
                  spectrum, GIALIAS_COLUMN_ERR_REDUCED, CPL_TYPE_DOUBLE,
                  GIALIAS_COLUMN_ERR_REDUCED_UNIT, NULL,
                  GIALIAS_COLUMN_ERR_REDUCED_TUTYP,
                  GIALIAS_COLUMN_ERR_REDUCED_TUCD, NULL);
    error |= irplib_sdp_spectrum_set_column_tcomm(
                  spectrum, GIALIAS_COLUMN_ERR_REDUCED, "");
    error |= irplib_sdp_spectrum_add_column(
                  spectrum, GIALIAS_COLUMN_SNR, CPL_TYPE_DOUBLE,
                  GIALIAS_COLUMN_SNR_UNIT, NULL, GIALIAS_COLUMN_SNR_TUTYP,
                  GIALIAS_COLUMN_SNR_TUCD, NULL);
    error |= irplib_sdp_spectrum_set_column_tcomm(
                  spectrum, GIALIAS_COLUMN_SNR, GIALIAS_COLUMN_SNR_TCOMM);
    cpl_error_ensure(! error, cpl_error_get_code(), goto cleanup,
                     "Could not setup the SDP spectrum columns.");

    indices = cpl_table_get_data_int_const(giraffe_table_get(fibertable),
                                           GIALIAS_COLUMN_INDEX);
    fps = cpl_table_get_data_int_const(giraffe_table_get(fibertable),
                                       GIALIAS_FPS);
    objects = cpl_table_get_data_string_const(giraffe_table_get(fibertable),
                                              GIALIAS_COLUMN_OBJECT);
    spectypes = cpl_table_get_data_string_const(giraffe_table_get(fibertable),
                                                GIALIAS_COLUMN_TYPE);
    ras = cpl_table_get_data_double_const(giraffe_table_get(fibertable),
                                          GIALIAS_COLUMN_RA);
    decs = cpl_table_get_data_double_const(giraffe_table_get(fibertable),
                                           GIALIAS_COLUMN_DEC);
    gcorr = cpl_table_get_data_double_const(giraffe_table_get(fibertable),
                                           GIALIAS_COLUMN_GCORR);
    hcorr = cpl_table_get_data_double_const(giraffe_table_get(fibertable),
                                           GIALIAS_COLUMN_HCORR);
    bcorr = cpl_table_get_data_double_const(giraffe_table_get(fibertable),
                                           GIALIAS_COLUMN_BCORR);
    cpl_error_ensure(indices != NULL && fps != NULL && objects != NULL
                     && spectypes != NULL && ras != NULL && decs != NULL
                     && gcorr != NULL && hcorr != NULL && bcorr != NULL,
                     cpl_error_get_code(), goto cleanup,
                     "Could not fetch data from the fiber setup table in '%s'.",
                     fibertable_name);

    formatstr = _giraffe_calc_format_string(
                            cpl_table_get_nrow(giraffe_table_get(fibertable)));

    /* Write the individual spectrum files: */
    filecount = 0;
    for (i = 0; i < cpl_table_get_nrow(giraffe_table_get(fibertable)); ++i) {
        const double* wave_data;
        double* flux_data;
        double* err_data;
        cpl_size j;
        double snr = 0.0;
        /* Keywords to remove: */
        const char* remregexp = "^(CDELT[0-9]+|CD[0-9]+_[0-9]+|CRPIX[0-9]+"
                                "|CRDER[0-9]+|CSYER[0-9]+|BUNIT|BSCALE|BZERO"
                                "|BLANK|FILTER)$";
        cpl_size specindex = indices[i];
        double vela, velb, beta;
        double correction_factor = 1.0;

        /* Skip non-science spectra. */
        if (strcmp(spectypes[i], "M") != 0) continue;

        filename = cpl_sprintf(formatstr, ++filecount);

        /* Calculate the Heliocentric correction factor to apply.
         * The gcorr and hcorr values are in km/s, so must be converted to m/s.
         */
        vela = gcorr[i] * 1e3;
        velb = hcorr[i] * 1e3;
        beta = (vela + velb) / CPL_PHYS_C;
        cpl_error_ensure(-1 <= beta && beta <= 1,
                         CPL_ERROR_ILLEGAL_OUTPUT, goto cleanup,
                         "The velocities GCORR = %g and HCORR = %g for spectrum"
                         "%"CPL_SIZE_FORMAT" in file '%s' give invalid"
                         " Heliocentric correction factor values.",
                         gcorr[i], hcorr[i], specindex, flux_filename);
        correction_factor = sqrt((1.0 + beta) / (1.0 - beta));

        /* Calculate and set corrected wavelength array, remembering to cast
         * to the appropriate type. */
        wave_data = cpl_array_get_data_double_const(refwavearray);
        if (wavecoltype == CPL_TYPE_FLOAT) {
            data_float = cpl_malloc(ny * sizeof(float));
            for (j = 0; j < ny; ++j) {
                data_float[j] = wave_data[j] * correction_factor;
            }
            array = cpl_array_wrap_float(data_float, ny);
        } else {
            data_double = cpl_malloc(ny * sizeof(double));
            for (j = 0; j < ny; ++j) {
                data_double[j] = wave_data[j] * correction_factor;
            }
            array = cpl_array_wrap_double(data_double, ny);
        }
        error |= irplib_sdp_spectrum_set_column_data(
                      spectrum, GIALIAS_COLUMN_WAVE, array);
        cpl_array_unwrap(array);
        array = NULL;

        fluximgcol = cpl_vector_new_from_image_column(
                          giraffe_image_get(fluximage), specindex);
        flux_data = cpl_vector_get_data(fluximgcol);
        cpl_error_ensure(flux_data != NULL, cpl_error_get_code(), goto cleanup,
                         "Unable to extract data in column %"CPL_SIZE_FORMAT
                         " from image in file '%s'.", specindex, flux_filename);
        array = cpl_array_wrap_double(flux_data, ny);
        error |= irplib_sdp_spectrum_set_column_data(
                      spectrum, GIALIAS_COLUMN_FLUX_REDUCED, array);
        cpl_array_unwrap(array);
        array = NULL;

        errimgcol = cpl_vector_new_from_image_column(
                          giraffe_image_get(errimage), specindex);
        err_data = cpl_vector_get_data(errimgcol);
        cpl_error_ensure(err_data != NULL, cpl_error_get_code(), goto cleanup,
                         "Unable to extract data in column %"CPL_SIZE_FORMAT
                         " from image in file '%s'.", specindex, err_filename);
        array = cpl_array_wrap_double(err_data, ny);
        error |= irplib_sdp_spectrum_set_column_data(
                      spectrum, GIALIAS_COLUMN_ERR_REDUCED, array);
        cpl_array_unwrap(array);
        array = NULL;

        data_double = cpl_malloc(ny * sizeof(double));
        for (j = 0; j < ny; ++j) {
            data_double[j] = (err_data[j] != 0.0) ? flux_data[j] / err_data[j]
                                                  : 0.0;
        }
        array = cpl_array_wrap_double(data_double, ny);
        snr = cpl_array_get_median(array);
        error |= irplib_sdp_spectrum_set_column_data(spectrum,
                                                     GIALIAS_COLUMN_SNR, array);
        cpl_array_unwrap(array);
        array = NULL;
        cpl_free(data_double);
        data_double = NULL;

        cpl_vector_delete(fluximgcol);
        fluximgcol = NULL;
        cpl_vector_delete(errimgcol);
        errimgcol = NULL;

        cpl_error_ensure(! error, cpl_error_get_code(), goto cleanup,
                         "Could not setup the SDP spectrum columns for '%s'.",
                         filename);

        error |= irplib_sdp_spectrum_set_object(spectrum, objects[i]);
        error |= irplib_sdp_spectrum_set_title(spectrum, objects[i]);
        error |= irplib_sdp_spectrum_set_ra(spectrum, ras[i]);
        error |= irplib_sdp_spectrum_set_dec(spectrum, decs[i]);
        error |= irplib_sdp_spectrum_set_snr(spectrum, snr);
        error |= irplib_sdp_spectrum_set_column_tcomm(
                      spectrum, GIALIAS_COLUMN_FLUX_REDUCED, flux_filename);
        error |= irplib_sdp_spectrum_set_column_tcomm(
                      spectrum, GIALIAS_COLUMN_ERR_REDUCED, err_filename);

        error |= cpl_propertylist_update_int(extrakeys, GIALIAS_FPS, fps[i]);
        error |= cpl_propertylist_update_double(extrakeys, GIALIAS_GEOCORR,
                                                gcorr[i]);
        error |= cpl_propertylist_update_double(extrakeys, GIALIAS_HELICORR,
                                                hcorr[i]);
        error |= cpl_propertylist_update_double(extrakeys, GIALIAS_BARYCORR,
                                                bcorr[i]);

        /* Set corrected values that depend on wavelengths. */
        error |= irplib_sdp_spectrum_set_wavelmin(spectrum,
                                                  wavelmin * correction_factor);
        error |= irplib_sdp_spectrum_set_wavelmax(spectrum,
                                                  wavelmax * correction_factor);
        error |= irplib_sdp_spectrum_set_specval(spectrum,
                            (wavelmax + wavelmin) * 0.5 * correction_factor);
        error |= irplib_sdp_spectrum_set_specbw(spectrum,
                                    (wavelmax - wavelmin) * correction_factor);
        error |= irplib_sdp_spectrum_set_tdmin(spectrum,
                                               wavelmin * correction_factor);
        error |= irplib_sdp_spectrum_set_tdmax(spectrum,
                                               wavelmax * correction_factor);
        error |= irplib_sdp_spectrum_set_specbin(spectrum,
                                                 specbin * correction_factor);
        if (! isnan(lamrms)) {
            error |= irplib_sdp_spectrum_set_lamrms(spectrum,
                                                    lamrms * correction_factor);
        }
        if (! isnan(specerr)) {
            error |= irplib_sdp_spectrum_set_specerr(spectrum,
                                                specerr * correction_factor);
        }
        if (! isnan(specsye)) {
            error |= irplib_sdp_spectrum_set_specsye(spectrum,
                                                specsye * correction_factor);
        }

        cpl_error_ensure(! error, cpl_error_get_code(), goto cleanup,
                         "Could not setup the SDP spectrum keywords for '%s'.",
                         filename);

        error |= irplib_dfs_save_spectrum(allframes, NULL, parlist, usedframes,
                                          inherit, spectrum, recipe_id,
                                          extrakeys, tablekeys, remregexp,
                                          pipe_id, dict_id, filename);
        cpl_error_ensure(! error, cpl_error_get_code(), goto cleanup,
                         "Failed to save SDP spectrum %"CPL_SIZE_FORMAT
                         " to file '%s'.", specindex, filename);

        error |= irplib_fits_update_checksums(filename);
        cpl_error_ensure(! error, cpl_error_get_code(), goto cleanup,
                         "Failed to save update checksums for file '%s'.",
                         filename);
        cpl_free(filename);
        filename = NULL;
    }

    if (filecount == 0) {
        cpl_msg_warning(cpl_func, "No science spectra found in '%s'."
                        " No SDP spectra created.", flux_filename);
    }

    result_code = 0;  /* Indicate success. */

cleanup:
    /* Cleanup objects and memory. Note: the delete functions already check for
     * NULL pointers. */
    cpl_vector_delete(fluximgcol);
    cpl_vector_delete(errimgcol);
    cpl_array_unwrap(array);
    cpl_array_delete(refwavearray);
    cpl_free(data_float);
    cpl_free(data_double);
    cpl_free(filename);
    cpl_free(formatstr);
    cpl_frameset_delete(usedframes);
    cpl_frameset_delete(rawframes);
    cpl_frameset_iterator_delete(iterator);
    cpl_free(pipe_id);
    cpl_propertylist_delete(tablekeys);
    cpl_propertylist_delete(extrakeys);
    giraffe_image_delete(fluximage);
    giraffe_image_delete(errimage);
    giraffe_table_delete(fibertable);
    irplib_sdp_spectrum_delete(spectrum);
    return result_code;
}
