/* $Id: kmo_priv_reconstruct.c,v 1.25 2013-09-12 14:42:03 aagudo Exp $
 *
 * This file is part of the KMOS Pipeline
 * Copyright (C) 2002,2003 European Southern Observatory
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

/*
 * $Author: aagudo $
 * $Date: 2013-09-12 14:42:03 $
 * $Revision: 1.25 $
 * $Name: not supported by cvs2svn $
 */

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

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

#include <cpl.h>

#include "kmclipm_constants.h"
#include "kmclipm_math.h"

#include <kmo_constants.h>
#include <kmo_cpl_extensions.h>
#include <kmo_debug.h>
#include <kmo_error.h>
#include <kmo_dfs.h>
#include <kmo_priv_reconstruct.h>
#include <kmo_priv_stats.h>

/*----------------------------------------------------------------------------*/
/**
    @defgroup kmos_priv_reconstruct     Helper functions for recipe kmo_reconstruct.

    @{
 */
/*----------------------------------------------------------------------------*/

int print_warning_once_reconstruct = TRUE;

/**
    @brief
        Create mask from XCAL frame.

    All invalid pixels are set to 0.0 as well as all pixels beloning to another IFUs.

    @param xcal    The xcalibration frame.
    @param ifu_id  The IFU id (between 1 and 8).

    @return The mask (has to be deallocated again).

    Possible cpl_error_code set in this function:

    @c CPL_ERROR_NULL_INPUT    Not enough inputs defined
    @c CPL_ERROR_ILLEGAL_INPUT index is larger or smaller than the size of
                               @c ifu_lambda_in
*/
cpl_image *kmo_create_mask_from_xcal(const cpl_image *xcal,
                                     int ifu_id)
{
    int         nx          = 0,
                ny          = 0,
                actual_ifu  = 0;
    double      tmp_dbl     = 0.0;
    float       *pmask      = NULL;
    const float *pxcal      = NULL;
    cpl_image   *mask       = NULL;

    KMO_TRY
    {
        // check inputs
        KMO_TRY_ASSURE(xcal != NULL,
                       CPL_ERROR_NULL_INPUT,
                       "Not all input data is provided!");

        nx = cpl_image_get_size_x(xcal);
        ny = cpl_image_get_size_y(xcal);
        KMO_TRY_CHECK_ERROR_STATE();

        KMO_TRY_ASSURE(//(ifu_id >= 1) &&
                       (ifu_id <= KMOS_IFUS_PER_DETECTOR),
                       CPL_ERROR_ILLEGAL_INPUT,
                       "ifu_id must be between 1 and 8!");

        // create mask
        KMO_TRY_EXIT_IF_NULL(
             mask = cpl_image_new(nx, ny, CPL_TYPE_FLOAT));
        KMO_TRY_EXIT_IF_ERROR(
            kmo_image_fill(mask, 0.));
        KMO_TRY_EXIT_IF_NULL(
             pmask = cpl_image_get_data_float(mask));
        KMO_TRY_EXIT_IF_NULL(
             pxcal = cpl_image_get_data_float_const(xcal));

        int i = 0;
        for (i = 0; i < nx*ny; i++) {
            if (ifu_id < 0) {
                if ((fabs(pxcal[i]) > 0.001) && !kmclipm_is_nan_or_inf(pxcal[i])) {
                    pmask[i] = 1.;
                }
            } else {
                // calculate actual IFU number
                tmp_dbl = (fabs(pxcal[i])-fabs((int)pxcal[i]))*10;
                actual_ifu = (int)(tmp_dbl + .5);
                if (actual_ifu == ifu_id) {
                    pmask[i] = 1.;
                }
            }
        }
    }
    KMO_CATCH
    {
        KMO_CATCH_MSG();
        cpl_image_delete(mask); mask = NULL;
    }

    return mask;
}

/**
    @brief
        Calculates the flux of a raw IFU image.

    This function calculates the flux of a section of a raw detector image,
    which encloses an IFU. On this section also parts of adjacent can be present.
    Therefore the ifu_id is checked using the xcal_mask which must be created with
    kmo_create_mask_from_xcal().

    @param data      The raw image data.
    @param xcal_mask The xcalibration mask (0: invalid, 1: valid).

    @return The flux of the raw IFU image. In case of an error 0.0 is returned
            and an error is set.

    Possible cpl_error_code set in this function:

    @c CPL_ERROR_NULL_INPUT    Not enough inputs defined
    @c CPL_ERROR_ILLEGAL_INPUT index is larger or smaller than the size of
                               @c ifu_lambda_in
*/
double kmo_calc_flux_in(const cpl_image *data,
                        const cpl_image *xcal_mask)
{
    int         nx          = 0,
                ny          = 0;
    double      flux        = 0.0;
    const float *pdata      = NULL,
                *pxcal_mask = NULL;

    KMO_TRY
    {
        // Check inputs
        KMO_TRY_ASSURE((data != NULL) &&
                       (xcal_mask != NULL),
                       CPL_ERROR_NULL_INPUT,
                       "Not all input data is provided!");

        nx = cpl_image_get_size_x(data);
        ny = cpl_image_get_size_y(data);
        KMO_TRY_CHECK_ERROR_STATE();

        KMO_TRY_ASSURE((nx == cpl_image_get_size_x(xcal_mask)) &&
                       (ny == cpl_image_get_size_y(xcal_mask)),
                       CPL_ERROR_ILLEGAL_INPUT,
                       "data and xcal_mask don't have the same size!");

        KMO_TRY_EXIT_IF_NULL(
            pdata = cpl_image_get_data_float_const(data));
        KMO_TRY_EXIT_IF_NULL(
            pxcal_mask = cpl_image_get_data_float_const(xcal_mask));

        int i = 0;
        for (i = 0;  i < nx*ny; i++) {
            if (pxcal_mask[i] > 0.5 ) {
                flux += pdata[i];
            }
        }
    }
    KMO_CATCH
    {
        KMO_CATCH_MSG();
        flux = 0.0;
    }

    return flux;
}

/**
    @brief
        Calculates the mode of a raw IFU image.

    This function calculates the mode of a section of a raw detector image,
    which encloses an IFU. On this section also parts of adjacent can be present.
    Therefore the ifu_id is checked using the xcal-frame.

    @param data    The raw image data.
    @param xcal    The xcalibration frame.
    @param ifu_id  The IFU id.

    @return The mode of the raw IFU image. In case of an error 0.0 is returned
            and an error is set.

    Possible cpl_error_code set in this function:

    @c CPL_ERROR_NULL_INPUT    Not enough inputs defined
    @c CPL_ERROR_ILLEGAL_INPUT index is larger or smaller than the size of
                               @c ifu_lambda_in
*/
double kmo_calc_mode_in(const cpl_image *data,
                        const cpl_image *xcal,
                        int ifu_id,
                        double *noise)
{
    int             nx          = 0,
                    ny          = 0,
                    actual_ifu  = 0,
                    nr_mask_pix = 0;
    double          mode        = 0.0,
                    tmp_dbl     = 0.0;
    const float     *pdata      = NULL,
                    *pxcal      = NULL;
    float           *pmask      = NULL;
    cpl_image       *mask       = NULL;
    kmclipm_vector  *datavec    = NULL;

    KMO_TRY
    {
        /* Check inputs */
        KMO_TRY_ASSURE((data != NULL) &&
                       (noise != NULL) &&
                       (xcal != NULL),
                       CPL_ERROR_NULL_INPUT,
                       "Not all input data is provided!");

        nx = cpl_image_get_size_x(data);
        ny = cpl_image_get_size_y(data);
        KMO_TRY_CHECK_ERROR_STATE();

        KMO_TRY_ASSURE((nx == cpl_image_get_size_x(xcal)) &&
                       (ny == cpl_image_get_size_y(xcal)),
                       CPL_ERROR_ILLEGAL_INPUT,
                       "data and xcal don't have the same size!");

        KMO_TRY_ASSURE((ifu_id >= 1) &&
                       (ifu_id <= KMOS_IFUS_PER_DETECTOR),
                       CPL_ERROR_ILLEGAL_INPUT,
                       "ifu_id must be between 1 and 8!");

        KMO_TRY_EXIT_IF_NULL(
            mask = cpl_image_new(nx, ny, CPL_TYPE_FLOAT));
        KMO_TRY_EXIT_IF_ERROR(
            kmo_image_fill(mask, 0.));
        KMO_TRY_EXIT_IF_NULL(
            pmask = cpl_image_get_data_float(mask));
        KMO_TRY_EXIT_IF_NULL(
            pdata = cpl_image_get_data_float_const(data));
        KMO_TRY_EXIT_IF_NULL(
            pxcal = cpl_image_get_data_float_const(xcal));

        // create mask based on xcal, valid values are set to 1
        int ix = 0, iy = 0;
        for (ix = 0;  ix < nx; ix++) {
            for (iy = 0;  iy < ny; iy++) {
                // calculate actual IFU number
                tmp_dbl = (fabs(pxcal[ix+iy*nx])-fabs((int)pxcal[ix+iy*nx]))*10;
                actual_ifu = (int)(tmp_dbl + .5);
                if (actual_ifu == ifu_id) {
                    pmask[ix+iy*nx] = 1.;
                }
            }
        }

        // calculate mode
        KMO_TRY_EXIT_IF_NULL(
            datavec = kmo_image_to_vector(data, mask, &nr_mask_pix));

        KMO_TRY_EXIT_IF_ERROR(
            kmo_calc_mode(datavec, &mode, noise,
                          DEF_POS_REJ_THRES, //cpos_rej,
                          DEF_NEG_REJ_THRES, //cneg_rej,
                          DEF_ITERATIONS)); //citer));
    }
    KMO_CATCH
    {
        KMO_CATCH_MSG();
        mode = 0.0;
    }

    kmclipm_vector_delete(datavec); datavec = NULL;
    cpl_image_delete(mask); mask = NULL;

    return mode;
}

///**
//    @brief
//        Calculates the WCS.
//
//    Alpha and delta are extracted from the @c main_header. crpix, cdelt
//    and CD matrix are calculated and inserted into @c sub_header.
//
//    @param main_header  The main header containing alpha and delta.
//    @param sub_header   (Updated) The sub header the new values are inserted in.
//    @param ifu_nr       The IFU number.
//    @param crval3       The start wavelength.
//    @param cdelt3       The wavelength increment.
//
//    @return
//        The function returns CPL_ERROR_NONE on success or a CPL error code
//        otherwise.
//
//    Possible cpl_error_code set in this function:
//
//    @li CPL_ERROR_NULL_INPUT     if @c main_header or @c sub_header is NULL.
//*/
//cpl_error_code kmo_calc_wcs(const cpl_propertylist *main_header,
//                            cpl_propertylist *sub_header,
//                            int ifu_nr,
//                            float crval3,
//                            float cdelt3)
//{
//    gridDefinition gd;
//
//    gd.x.start = -1300;
//    gd.x.delta = 200;
//    gd.x.dim = 14;
//    gd.y.start = -1300;
//    gd.y.delta = 200;
//    gd.y.dim = 14;
//    gd.l.start = crval3;
//    gd.l.delta = cdelt3;
//    gd.l.dim = 14;
//
//    return kmo_calc_wcs_gd(main_header, sub_header, ifu_nr, gd);
//}

/**
    @brief
        Calculates the WCS.

    Alpha and delta are extracted from the @c main_header. crpix, cdelt
    and CD matrix are calculated and inserted into @c sub_header.

    @param main_header  The main header containing alpha and delta.
    @param sub_header   (Updated) The sub header the new values are inserted in.
    @param ifu_nr       The IFU number.
    @param gd           grid definition of the data cube

    @return
        The function returns CPL_ERROR_NONE on success or a CPL error code
        otherwise.

    Possible cpl_error_code set in this function:

    @li CPL_ERROR_NULL_INPUT     if @c main_header or @c sub_header is NULL.
*/
cpl_error_code kmo_calc_wcs_gd(const cpl_propertylist *main_header,
                            cpl_propertylist *sub_header,
                            int ifu_nr,
                            gridDefinition gd)
{
    cpl_error_code  ret_error       = CPL_ERROR_NONE;

    int              det_nr                 = 0,
                     calc_wcs               = FALSE;

    float            crval3                 = 0.0,
                     cdelt3                 = 0.0;

    double           alpha                  = 0.0,
                     delta                  = 0.0,
                     crval1                 = 0.0,
                     crval2                 = 0.0,
                     cdelt1                 = 0.0,
                     cdelt2                 = 0.0,
                     crpix1                 = 0.0,
                     crpix2                 = 0.0,
                     crpix3                 = 0.0,
                     offangle                  = 0.0,
                     ditha                  = 0.0,
                     dithd                  = 0.0;

    char             *keyword               = NULL,
                     *keyword_alpha         = NULL,
                     *keyword_delta         = NULL;

    const char       *tmp_str               = NULL;

    KMO_TRY
    {
        /* Check inputs */
        KMO_TRY_ASSURE((main_header != NULL) &&
                       (sub_header != NULL),
                       CPL_ERROR_NULL_INPUT,
                       "Not all input data is provided!");

        KMO_TRY_ASSURE((ifu_nr > 0) &&
                       (ifu_nr <= KMOS_NR_IFUS),
                       CPL_ERROR_ILLEGAL_INPUT,
                       "ifu_nr must be from 1-24!");

        // extract and save same name
        KMO_TRY_EXIT_IF_NULL(
            keyword = cpl_sprintf("%s%d%s",IFU_NAME_PREFIX, ifu_nr, IFU_NAME_POSTFIX));

        if (cpl_propertylist_has(main_header, keyword)) {
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_string(sub_header,
                                        keyword,
                                        cpl_propertylist_get_string(main_header,
                                                                    keyword),
                                        cpl_propertylist_get_comment(main_header,
                                                                     keyword)));
            KMO_TRY_CHECK_ERROR_STATE();
        } else {
            if (print_warning_once_reconstruct) {
                cpl_msg_warning("", "Keyword '%s' missing!", keyword);
            }
        }
        cpl_free(keyword); keyword = NULL;

        KMO_TRY_EXIT_IF_NULL(
            keyword_alpha = cpl_sprintf("%s%d%s",IFU_ALPHA_PREFIX, ifu_nr,
                                        IFU_ALPHA_POSTFIX));
        KMO_TRY_EXIT_IF_NULL(
            keyword_delta = cpl_sprintf("%s%d%s",IFU_DELTA_PREFIX, ifu_nr,
                                        IFU_DELTA_POSTFIX));

        if (cpl_propertylist_has(main_header, keyword_alpha)) {
            if (cpl_propertylist_has(main_header, keyword_delta)) {
                calc_wcs = TRUE;
                KMO_TRY_EXIT_IF_ERROR(
                    kmclipm_update_property_double(sub_header,
                                    keyword_alpha,
                                    cpl_propertylist_get_double(main_header,
                                                               keyword_alpha),
                                    cpl_propertylist_get_comment(main_header,
                                                               keyword_alpha)));
                KMO_TRY_EXIT_IF_ERROR(
                    kmclipm_update_property_double(sub_header,
                                    keyword_delta,
                                    cpl_propertylist_get_double(main_header,
                                                               keyword_delta),
                                    cpl_propertylist_get_comment(main_header,
                                                               keyword_delta)));
            } else {
                if (print_warning_once_reconstruct) {
                    cpl_msg_warning("", "Keyword '%s' is missing (probably as "
                                        "well for the other IFUs), WCS "
                                        "information won't be calculated for "
                                        "axis 1 and 2!", keyword_delta);
                    print_warning_once_reconstruct = FALSE;
                }
            }
        } else {
            if (print_warning_once_reconstruct) {
                cpl_msg_warning("", "Keyword '%s' is missing (probably as well "
                                    "for the other IFUs), WCS information "
                                    "won't be calculated for axis 1 and 2!",
                                    keyword_alpha);
                print_warning_once_reconstruct = FALSE;
            }
        }

        if (calc_wcs) {
            /* calculate WCS */
            // extract alpha, delta & rotation
            alpha = cpl_propertylist_get_double(main_header, keyword_alpha);
            KMO_TRY_CHECK_ERROR_STATE();

            delta = cpl_propertylist_get_double(main_header, keyword_delta);
            KMO_TRY_CHECK_ERROR_STATE();

            // calculate DEC
//            crpix2 = KMOS_SLITLET_Y/2.+0.5;
//            cdelt2 = (double)KMOS_PIX_RESOLUTION/60/60;
            crpix2 = (-gd.y.start / gd.y.delta) + 1;
            cdelt2 = (double) (gd.y.delta) /1000./60./60.;
            crval2 = kmo_to_deg(delta);
            if (cpl_propertylist_has(main_header, DITHD) == 1) {
                // if DITHD is present add it to DEC (after converting arcsec
                // into deg)
                dithd = cpl_propertylist_get_double(main_header, DITHD);
                crval2 += dithd/3600;
            } else {
                if (print_warning_once_reconstruct) {
                    cpl_msg_warning("", "Keyword '%s' is missing, calculated "
                                    "WCS won't reflect dithering!", DITHD);
                    print_warning_once_reconstruct = FALSE;
                }
            }

            // calculate RA here
//            crpix1 = KMOS_SLITLET_X/2.+0.5;
//            cdelt1 = -(double)KMOS_PIX_RESOLUTION/60/60;
            crpix1 = (-gd.x.start / gd.x.delta) + 1;
            cdelt1 = (double) (-gd.x.delta) /1000./60./60.;
            crval1 = kmo_to_deg(alpha)*15;
            if (cpl_propertylist_has(main_header, DITHA) == 1) {
                // if DITHA is present add it to RA (after converting arcsec
                // into deg and dividing by cosine)
                ditha = cpl_propertylist_get_double(main_header, DITHA);
                crval1 += ditha/3600/cos(crval2*CPL_MATH_PI/180.);
            } else {
                if (print_warning_once_reconstruct) {
                    cpl_msg_warning("", "Keyword '%s' is missing, calculated "
                                    "WCS won't reflect dithering!", DITHA);
                    print_warning_once_reconstruct = FALSE;
                }
            }
    //        cdelt1 = -(double)KMOS_PIX_RESOLUTION/60/60*15
    //                                            *cos(crval2*CPL_MATH_PI/180.);

            crpix3 = 1;
            crval3 = gd.l.start;
            cdelt3 = gd.l.delta;

//            printf("WCS: %f %f %f    %f %f %f    %f %f %f\n",
//                    crpix1, crval1, cdelt1, crpix2, crval2, cdelt2, crpix3, crval3, cdelt3 );
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CRVAL1, crval1,
                                               "[deg] RA at ref. pixel"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CRVAL2, crval2,
                                               "[deg] DEC at ref. pixel"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CRVAL3, crval3,
                                               "[um] Wavelength at ref. pixel"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CRPIX1, crpix1,
                                               "[pix] Reference pixel in x"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CRPIX2, crpix2,
                                               "[pix] Reference pixel in y"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CRPIX3, crpix3,
                                               "[pix] Reference pixel in z"));

            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CDELT1, cdelt1,
                                               "[deg] Pixel resolution in x"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CDELT2, cdelt2,
                                               "[deg] Pixel resolution in y"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CDELT3, cdelt3,
                                               "[um] Spectral resolution"));

            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_string(sub_header,
                                        CTYPE1,
                                        "RA---TAN",
                                        "TAN projection used"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_string(sub_header,
                                        CTYPE2,
                                        "DEC--TAN",
                                        "TAN projection used"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_string(sub_header,
                                        CTYPE3,
                                        "WAVE",
                                        "Coordinate system of z-axis"));

            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_string(sub_header, CUNIT1,
                                               "deg", "Unit of x-axis"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_string(sub_header, CUNIT2,
                                               "deg", "Unit of y-axis"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_string(sub_header, CUNIT3,
                                               "um", "Unit of z-axis"));

            KMO_TRY_CHECK_ERROR_STATE();
            offangle = cpl_propertylist_get_double(main_header, OFFANGLE);
            if (cpl_error_get_code() != CPL_ERROR_NONE) {
                offangle = 0.0;
                if (print_warning_once_reconstruct) {
                    cpl_msg_warning("","Keyword '%s' missing in "
                                       "primary header! Setting the value to 0.0.",
                                       OFFANGLE);
                    print_warning_once_reconstruct = FALSE;
                }
                cpl_error_reset();
            }
            KMO_TRY_CHECK_ERROR_STATE();

            // CD1_1 CD1_2 CD1_3
            // CD2_1 CD2_2 CD2_3
            // CD3_1 CD3_2 CD3_3
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CD1_1,
                                               cdelt1*cos(-offangle*CPL_MATH_PI/180.0),
                                               "[] x-component of East"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CD1_2,
                                               -cdelt2*sin(-offangle*CPL_MATH_PI/180.0),
                                                "[] y-component of East"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CD1_3, 0.0,
                                               "[] no rotation"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CD2_1,
                                               cdelt1*sin(-offangle*CPL_MATH_PI/180.0),
                                                "[] x-component of North"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CD2_2,
                                               cdelt2*cos(-offangle*CPL_MATH_PI/180.0),
                                               "[] y-component of North"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CD2_3, 0.0,
                                               "[] no rotation"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CD3_1, 0.0,
                                               "[] no rotation"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CD3_2, 0.0,
                                               "[] no rotation"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CD3_3, cdelt3,
                                               "[] no rotation"));
        } else {
  /*          KMO_TRY_EXIT_IF_ERROR(
                cpl_propertylist_erase(sub_header, CRPIX1));
            KMO_TRY_EXIT_IF_ERROR(
                cpl_propertylist_erase(sub_header, CRPIX2));
*/
            crpix3 = 1;
            crval3 = gd.l.start;
            cdelt3 = gd.l.delta;

            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CRVAL3, crval3,
                                               "[um] Wavelength at ref. pixel"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CRPIX3, crpix3,
                                               "[pix] Reference pixel in z"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CDELT3, cdelt3,
                                               "[um] Spectral resolution"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_string(sub_header,
                                        CTYPE3,
                                        "WAVE",
                                        "Coordinate system of z-axis"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CRVAL1, 1,
                                               "[deg] RA at ref. pixel"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CRVAL2, 1,
                                               "[deg] DEC at ref. pixel"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CRPIX1, 1,
                                               "[pix] Reference pixel in x"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CRPIX2, 1,
                                               "[pix] Reference pixel in y"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CDELT1, 1,
                                               "[deg] Pixel resolution in x"));
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_double(sub_header, CDELT2, 1,
                                               "[deg] Pixel resolution in y"));
        } // end if calc_wcs

        // add filter wheel keywords
        det_nr = (ifu_nr-1)/KMOS_IFUS_PER_DETECTOR + 1;

        // copy ESO INS FILTi ID from main_header to sub_header if present
        KMO_TRY_EXIT_IF_NULL(
            keyword = cpl_sprintf("%s%d%s",IFU_FILTID_PREFIX, det_nr, IFU_FILTID_POSTFIX));
        tmp_str = cpl_propertylist_get_string(main_header, keyword);
        if (tmp_str != NULL) {
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_string(sub_header,
                                        keyword,
                                        tmp_str,
                                        cpl_propertylist_get_comment(main_header,
                                                                     keyword)));
            KMO_TRY_CHECK_ERROR_STATE();
        } else {
            cpl_error_reset();
        }
        cpl_free(keyword); keyword = NULL;
        // copy ESO INS GRATi ID from main_header to sub_header if present
        KMO_TRY_EXIT_IF_NULL(
            keyword = cpl_sprintf("%s%d%s",IFU_GRATID_PREFIX, det_nr, IFU_GRATID_POSTFIX));
        tmp_str = cpl_propertylist_get_string(main_header, keyword);
        if (tmp_str != NULL) {
            KMO_TRY_EXIT_IF_ERROR(
                kmclipm_update_property_string(sub_header,
                                        keyword,
                                        tmp_str,
                                        cpl_propertylist_get_comment(main_header,
                                                                     keyword)));
            KMO_TRY_CHECK_ERROR_STATE();
        } else {
            cpl_error_reset();
        }
        cpl_free(keyword); keyword = NULL;
        cpl_free(keyword_alpha); keyword_alpha = NULL;
        cpl_free(keyword_delta); keyword_delta = NULL;
    }
    KMO_CATCH
    {
        KMO_CATCH_MSG();
        ret_error = cpl_error_get_code();
    }

    return ret_error;
}

cpl_error_code kmo_save_det_img_ext(cpl_image * det_img,
                                    gridDefinition gd,
                                    int dev,
                                    const char *filename,
                                    const char *obs_suffix,
                                    cpl_propertylist *header,
                                    int dev_flip,
                                    int is_noise)
{
    cpl_propertylist    *hdr = NULL;
    char                *extname    = NULL;
    int                 crpix   = 1;
    double              cdelt   = 0.;

    KMO_TRY {
        cdelt = gd.l.delta;
        crpix = 1;
        if (dev_flip == FALSE) {
            KMO_TRY_EXIT_IF_ERROR(
                cpl_image_flip(det_img, 0));
            cdelt = -cdelt;
            crpix = gd.l.dim;
        }

        KMO_TRY_EXIT_IF_NULL(
            hdr = cpl_propertylist_duplicate(header));

        if (!is_noise) {
            KMO_TRY_EXIT_IF_NULL(
                extname = cpl_sprintf("DET.%d.DATA",dev));
        } else {
            KMO_TRY_EXIT_IF_NULL(
                extname = cpl_sprintf("DET.%d.NOISE",dev));
        }
        KMO_TRY_EXIT_IF_ERROR(
            kmclipm_update_property_string(hdr, "EXTNAME", extname,
                                           "FITS extension name"));
        cpl_free(extname); extname = NULL;

        KMO_TRY_EXIT_IF_ERROR(
            kmclipm_update_property_string(hdr, "CTYPE1", "PIXEL", ""));
        KMO_TRY_EXIT_IF_ERROR(
            kmclipm_update_property_string(hdr, "CTYPE2", "WAVELEN", ""));
        KMO_TRY_EXIT_IF_ERROR(
            kmclipm_update_property_double(hdr, "CRPIX1", 0.0, ""));
        KMO_TRY_EXIT_IF_ERROR(
            kmclipm_update_property_double(hdr, "CRPIX2", crpix, ""));
        KMO_TRY_EXIT_IF_ERROR(
            kmclipm_update_property_double(hdr, "CRVAL1", 0.0, ""));
        KMO_TRY_EXIT_IF_ERROR(
            kmclipm_update_property_double(hdr, "CRVAL2", gd.l.start, ""));
        KMO_TRY_EXIT_IF_ERROR(
            kmclipm_update_property_double(hdr, "CDELT1", 1.0, ""));
        KMO_TRY_EXIT_IF_ERROR(
            kmclipm_update_property_double(hdr, "CDELT2", cdelt, ""));
        KMO_TRY_EXIT_IF_ERROR(
            kmo_dfs_save_image(det_img, filename, obs_suffix, hdr, 0./0.));
    } KMO_CATCH {
        cpl_msg_error(cpl_func, "Encountered error: %s in %s",
                      cpl_error_get_message(), cpl_error_get_where());
    }

    cpl_propertylist_delete(hdr); hdr = NULL;

    return cpl_error_get_code();
}

/** @} */
