/*
 * This file is part of the FORS Library
 * Copyright (C) 2002-2010 European Southern Observatory
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
 */

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

#include <fors_zeropoint_impl.h>

#include <fors_extract.h>
#include <fors_identify.h>
#include <fors_tools.h>
#include <fors_star.h>
#include <fors_std_cat.h>
#include <fors_std_star.h>
#include <fors_image.h>
#include <fors_instrument.h>
#include <fors_data.h>
#include <fors_setting.h>
#include <fors_dfs.h>
#include <fors_pfits.h>
#include <fors_utils.h>
#include "fors_ccd_config.h"
#include "fors_ccd_config.h"
#include "fors_overscan.h"
#include "fors_subtract_bias.h"
#include "fors_detmodel.h"


#include <cpl.h>

#include <math.h>

/**
 * @addtogroup fors_zeropoint
 */

/**@{*/

const char *const fors_zeropoint_name = "fors_zeropoint";
const char *const fors_zeropoint_description_short = "Compute zeropoint";
const char *const fors_zeropoint_author = "Jonas M. Larsen";
const char *const fors_zeropoint_email = PACKAGE_BUGREPORT;
const char *const fors_zeropoint_description = 
"Input files:\n"
"  DO category:               Type:       Explanation:              Number:\n"
"  STANDARD_IMG               FITS image  Phot. standard field        1\n"
"  MASTER_BIAS                FITS image  Master bias                 1\n"
"  MASTER_SKY_FLAT_IMG        FITS image  Master sky flatfield        1\n"
"  FLX_STD_IMG                FITS table  Standard star catalog       1+\n"
"  PHOT_TABLE                 FITS table  Filter ext. coeff, color    1\n"
"\n"
"Output files:\n"
"  DO category:               Data type:  Explanation:\n"
"  SOURCES_STD_IMG            FITS image  Unfiltered SExtractor output\n"
"  ALIGNED_PHOT               FITS table\n"
"  PHOT_BACKGROUND_STD_IMG    FITS image  Reduced science image background\n"
"  STANDARD_REDUCED_IMG       FITS image  Reduced std image\n";

static double
get_zeropoint(fors_star_list *stars, 
              double cutoffE,
              double cutoffk,
              double dext_coeff,
              double dcolor_term,
              double avg_airmass,
              double *dzeropoint,
              int *n);

static cpl_error_code
fors_zeropoint_astrometry(                  const cpl_frameset  *std_cat_frames,
                                            char                filter_band,
                                            double              color_correct,
                                            double              dcolor_correct,
                                            const identify_method
                                                                *id_method,
                                            fors_star_list      *extracted,
                                            cpl_propertylist    *wcs_header,
                                            fors_std_star_list  **std_cat,
                                            cpl_image           **histogram);

static cpl_error_code
fors_zeropoint_astrometry_get_wcs_shift_px( const fors_star_list    *stars,
                                            double                  *dx,
                                            double                  *dy);

static cpl_error_code
fors_zeropoint_astrometry_shift_wcs_origin( cpl_propertylist    *header,
                                            double  dx,
                                            double  dy);

static cpl_error_code
fors_zeropoint_astrometry_apply_unidentified_xy2radec(
                                            fors_star_list          *stars,
                                            const cpl_propertylist  *header);

void
fors_zeropoint_errorstate_dump_as_warning(  unsigned self,
                                            unsigned first,
                                            unsigned last);

/**
 * @brief    Define recipe parameters
 * @param    parameters     parameter list to fill
 */
void fors_zeropoint_define_parameters(cpl_parameterlist *parameters)
{
    const char *context = cpl_sprintf("fors.%s", fors_zeropoint_name);
    
    fors_extract_define_parameters(parameters, context);

    fors_identify_define_parameters(parameters, context);
    
    const char *name, *full_name;
    cpl_parameter *p;

    name = "magcutE";
    full_name = cpl_sprintf("%s.%s", context, name);
    p = cpl_parameter_new_value(full_name,
                                CPL_TYPE_DOUBLE,
                                "Zeropoint absolute cutoff (magnitude)",
                                context,
                                1.0);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, name);
    cpl_parameter_disable(p, CPL_PARAMETER_MODE_ENV);
    cpl_parameterlist_append(parameters, p);
    cpl_free((void *)full_name); full_name = NULL;

    name = "magcutk";
    full_name = cpl_sprintf("%s.%s", context, name);
    p = cpl_parameter_new_value(full_name,
                                CPL_TYPE_DOUBLE,
                                "Zeropoint kappa rejection parameter",
                                context,
                                5.0);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, name);
    cpl_parameter_disable(p, CPL_PARAMETER_MODE_ENV);
    cpl_parameterlist_append(parameters, p);
    cpl_free((void *)full_name); full_name = NULL;

    name = "magsyserr";
    full_name = cpl_sprintf("%s.%s", context, name);
    p = cpl_parameter_new_value(full_name,
                                CPL_TYPE_DOUBLE,
                                "Systematic error in magnitude",
                                context,
                                0.01);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, name);
    cpl_parameter_disable(p, CPL_PARAMETER_MODE_ENV);
    cpl_parameterlist_append(parameters, p);
    cpl_free((void *)full_name); full_name = NULL;


    cpl_free((void *)context);

    return;
}

#undef cleanup
#define cleanup \
do { \
    cpl_frameset_delete(std_frame); \
    cpl_frameset_delete(master_bias_frame); \
    cpl_frameset_delete(master_flat_frame); \
    cpl_frameset_delete(std_cat_frames); \
    cpl_frameset_delete(phot_table); \
    fors_image_delete(&std); \
    fors_image_delete(&master_bias); \
    fors_image_delete(&master_flat); \
    cpl_table_delete(aligned_phot); \
    cpl_image_delete(background); \
    cpl_table_delete(sources); \
    fors_extract_method_delete(&em); \
    fors_identify_method_delete(&im); \
    fors_std_star_list_delete(&cat, fors_std_star_delete); \
    /* All std-stars (and non-std-stars) are linked by the respective star */ \
    /* objects in "stars", without being referenced by a std-star-list */ \
    /* object. So they are deleted together with the function */ \
    /* fors_star_delete() while deleting the list "stars". */ \
    fors_star_list_delete(&stars, fors_star_delete); \
    cpl_free((void *)context); \
    fors_setting_delete(&setting); \
    cpl_propertylist_delete(qc_phot); \
    cpl_propertylist_delete(qc_sources); \
    cpl_propertylist_delete(product_header); \
    cpl_propertylist_delete(raw_header); \
} while (0)

/**
 * @brief    Do the processing
 *
 * @param    frames         input frames
 * @param    parameters     recipe parameters
 *
 * @return   0 if everything is ok
 */
void fors_zeropoint(cpl_frameset *frames, const cpl_parameterlist *parameters)
{
    /* Raw */
    cpl_frameset *std_frame      = NULL;
    fors_image *std              = NULL;

    /* Calibration */
    cpl_frameset *master_bias_frame = NULL;
    fors_image *master_bias   = NULL; 

    cpl_frameset *master_flat_frame = NULL;
    fors_image *master_flat         = NULL; 

    cpl_frameset *std_cat_frames    = NULL;
    fors_std_star_list *cat         = NULL;

    cpl_frameset *phot_table        = NULL;
    double color_term, dcolor_term;
    double ext_coeff, dext_coeff;
    double expected_zeropoint, dexpected_zeropoint;
    
    /**/
    cpl_propertylist *raw_header  = NULL;
    
    /* Products */
    int nzeropoint = -1;             /* Suppress warning */
    cpl_table *aligned_phot = NULL;
    cpl_propertylist *qc_phot = NULL;
    cpl_propertylist *qc_sources = NULL;
    double zeropoint = 0.0, dzeropoint;
    fors_extract_sky_stats sky_stats;
    cpl_image *background = NULL;
    cpl_table *sources    = NULL;
    cpl_propertylist *product_header = NULL;
    cpl_image *histogram = NULL;
        
    /* Parameters */
    extract_method  *em = NULL;
    identify_method *im = NULL;
    double cutoffE, cutoffk;
    double magsyserr;

    /* Other */
    const char *context   = NULL;
    fors_star_list *stars = NULL;
    fors_setting *setting = NULL;
    double avg_airmass = 0.0;
    const char *target_name = NULL; 
    
    qc_phot = cpl_propertylist_new();
    qc_sources = cpl_propertylist_new();
    product_header = cpl_propertylist_new();
    context = cpl_sprintf("fors.%s", fors_zeropoint_name);
    
    /* Get parameters */
    em = fors_extract_method_new(parameters, context);
    assure( !cpl_error_get_code(), return, 
            "Could not get extraction parameters" );
    
    im = fors_identify_method_new(parameters, context);
    assure( !cpl_error_get_code(), return, 
            "Could not get identification parameters" );


    cpl_msg_indent_more();
    const char *name = cpl_sprintf("%s.%s", context, "magcutE");
    cutoffE = dfs_get_parameter_double_const(parameters, 
                                            name);
    cpl_free((void *)name); name = NULL;
    cpl_msg_indent_less();
    assure( !cpl_error_get_code(), return, NULL );
        
    cpl_msg_indent_more();
    name = cpl_sprintf("%s.%s", context, "magcutk");
    cutoffk = dfs_get_parameter_double_const(parameters, 
                                             name);
    cpl_free((void *)name); name = NULL;
    cpl_msg_indent_less();
    assure( !cpl_error_get_code(), return, NULL );
        
    cpl_msg_indent_more();
    name = cpl_sprintf("%s.%s", context, "magsyserr");
    magsyserr = dfs_get_parameter_double_const(parameters, 
                                               name);
    cpl_free((void *)name); name = NULL;
    cpl_msg_indent_less();
    assure( !cpl_error_get_code(), return, NULL );
    assure( magsyserr >= 0, return, 
            "Input systematic error (magsyserr=%f) cannot be negative",
            magsyserr);
        
    /* Find raw */
    std_frame = fors_frameset_extract(frames, STANDARD_IMG);
    assure( cpl_frameset_get_size(std_frame) == 1, return, 
            "Exactly 1 %s required. %"CPL_SIZE_FORMAT" found", 
            STANDARD_IMG, cpl_frameset_get_size(std_frame) );

    /* Find calibration */
    master_bias_frame = fors_frameset_extract(frames, MASTER_BIAS);
    assure( cpl_frameset_get_size(master_bias_frame) == 1, return, 
            "One %s required. %"CPL_SIZE_FORMAT" found", 
            MASTER_BIAS, cpl_frameset_get_size(master_bias_frame) );

    master_flat_frame = fors_frameset_extract(frames, MASTER_SKY_FLAT_IMG);
    assure( cpl_frameset_get_size(master_flat_frame) == 1, return, 
            "One %s required. %"CPL_SIZE_FORMAT" found", 
            MASTER_SKY_FLAT_IMG, cpl_frameset_get_size(master_flat_frame) );

    std_cat_frames = fors_frameset_extract(frames, FLX_STD_IMG);
    assure( cpl_frameset_get_size(std_cat_frames) >= 1, return, 
            "One or more %s required. %"CPL_SIZE_FORMAT" found",
            FLX_STD_IMG, cpl_frameset_get_size(std_cat_frames));

    phot_table = fors_frameset_extract(frames, PHOT_TABLE);
    assure( cpl_frameset_get_size(phot_table) == 1, return, 
            "One %s required. %"CPL_SIZE_FORMAT" found",
            PHOT_TABLE, cpl_frameset_get_size(phot_table));

    /* Done finding frames */

    /* Get setting */
    setting = fors_setting_new(cpl_frameset_get_position(std_frame, 0));
    assure( !cpl_error_get_code(), return, "Could not get instrument setting" );
    
    /* Load std raw frame header */
    raw_header = cpl_propertylist_load(
                    cpl_frame_get_filename(
                        cpl_frameset_get_position(std_frame, 0)), 0);
    if (raw_header == NULL) {
        cpl_msg_error(cpl_func, "Failed to load raw header");
        cleanup;
        return;
    }
    fors::fiera_config ccd_config(raw_header);

    /* Update RON estimation from bias */
    cpl_propertylist * master_bias_header =
       cpl_propertylist_load(cpl_frame_get_filename(
               cpl_frameset_get_position(master_bias_frame, 0)), 0);
    fors::update_ccd_ron(ccd_config, master_bias_header);
    cpl_propertylist_delete(master_bias_header);
    assure( !cpl_error_get_code(), return, "Could not get RON from master bias"
            " (missing QC DET OUT? RON keywords)");


    /* Getting info from std header */
    avg_airmass = fors_get_airmass(raw_header);
    target_name = cpl_propertylist_get_string(raw_header, FORS_PFITS_TARG_NAME);
    cpl_msg_info(cpl_func, "Target name: %s", target_name);
    
    /* Load master bias */
    master_bias = 
         fors_image_load(cpl_frameset_get_position(master_bias_frame, 0));
    assure( !cpl_error_get_code(), return, 
            "Could not load master bias");

    /* Load raw frames, subtract overscan and bias */
    fors_image * std_raw = 
        fors_image_load(cpl_frameset_get_position(std_frame, 0));
    /* Create variance map */
    fors_image_variance_from_detmodel(std_raw, ccd_config);
    /*Subtract overscan */
    std = fors_subtract_prescan(std_raw, ccd_config);
    fors_trimm_preoverscan(std, ccd_config);
    fors_image_delete(&std_raw);
    /* Subtract bias */
    fors_subtract_bias(std, master_bias);
    assure( !cpl_error_get_code(), return, "Could not load standard image");
    fors_image_delete(&master_bias);

    /* Load master flat */
    master_flat = 
        fors_image_load(cpl_frameset_get_position(master_flat_frame, 0));
    assure( !cpl_error_get_code(), return, "Could not load master flat");
    
    /* Divide by flat */
    fors_image_divide_scalar(master_flat,
                             fors_image_get_median(master_flat, NULL), -1.0);
    fors_image_divide(std, master_flat);
    assure( !cpl_error_get_code(), return, "Could not divide by master flat");
    fors_image_delete(&master_flat);

    /* Extract sources */
    stars = fors_extract(std, setting, em, magsyserr,
                         &sky_stats, &background, &sources);
    assure( !cpl_error_get_code(), return, "Could not extract objects");


    if (setting->filter_name != NULL)
    {
        char            filter_band;
        cpl_errorstate  local_ers = cpl_errorstate_get();
        
        /* load raw frame header */

/* Moved outside by Carlo - start

        raw_header = cpl_propertylist_load(
                        cpl_frame_get_filename(
                            cpl_frameset_get_first(std_frame)), 0);
        assure(                             cpl_errorstate_is_equal(local_ers),
                                            return,
                                            "Failed to load raw header");
Moved outside by Carlo - end */
        
        /* Load filter coefficients */
        fors_phot_table_load(cpl_frameset_get_position(phot_table, 0), setting,
                             &color_term, &dcolor_term,
                             &ext_coeff, &dext_coeff,
                             &expected_zeropoint, &dexpected_zeropoint);
        assure(                             cpl_errorstate_is_equal(local_ers),
                                            return,
                                            "Could not load photometry table" );

        filter_band = fors_instrument_filterband_get_by_setting(setting);
        
        /* Do the whole astrometry:
         * load catalogue, apply wcs, do pattern-matching, correct wcs
         * (treat errors only as warnings)
         */
        cpl_msg_info(cpl_func, "Astrometry:");
        cpl_msg_indent_more();
        fors_zeropoint_astrometry(          std_cat_frames,
                                            filter_band,
                                            color_term,
                                            dcolor_term,
                                            im,
                                            stars,      /* sources */
                                            raw_header, /* wcs */
                                            &cat,       /* to draw debug-img */
                                            &histogram);
        cpl_msg_indent_less();
        if (!cpl_errorstate_is_equal(local_ers))
        {
            cpl_msg_warning(cpl_func, "Astrometric calibration failed:");
            cpl_msg_indent_more();
            cpl_errorstate_dump(local_ers,
                                CPL_FALSE,
                                fors_zeropoint_errorstate_dump_as_warning);
            cpl_msg_indent_less();
            /* reset error */
            cpl_errorstate_set(local_ers);
        }
        /* The astrometric calibration could fail but nonetheless have
         * succeeded in identifying some standard stars. So continue trying to
         * get the zeropoint anyway. */

        /* Correct for atmospheric extinction, gain, exposure time */
        /* FIXME: FAP: use WCS corrected header */
        avg_airmass = fors_star_ext_corr(   stars,
                                            setting,
                                            ext_coeff,
                                            dext_coeff,
                                            cpl_frameset_get_position(std_frame, 0));
        /* Get zeropoint. */
        if (cpl_errorstate_is_equal(local_ers))
        {
            zeropoint = get_zeropoint(      stars,
                                            cutoffE,
                                            cutoffk,
                                            dext_coeff,
                                            dcolor_term,
                                            avg_airmass,
                                            &dzeropoint,
                                            &nzeropoint);
        }
        
        if (!cpl_errorstate_is_equal(local_ers))
        {
            cpl_msg_warning(cpl_func, "Failed to get zeropoint");
            cpl_msg_indent_more();
            cpl_errorstate_dump(local_ers,
                                CPL_FALSE,
                                fors_zeropoint_errorstate_dump_as_warning);
            cpl_msg_indent_less();
            /* reset error */
            cpl_errorstate_set(local_ers);
        }
    }
    else {
       cpl_msg_warning(cpl_func, "Zeropoint computation is not supported "
                       "for non-standard filters");
       color_term = 0.0;
       dcolor_term = 9999.0;
       ext_coeff = 0.0;
       dext_coeff = 9999.0;
       expected_zeropoint = 0.0;
       dexpected_zeropoint = 9999.0;
       zeropoint = 0.0;
       dzeropoint = 0.0;
       nzeropoint = 0;
    }

    /* QC */
    
    cpl_msg_info(cpl_func,"Frame zeropoint = %f mag", zeropoint);
    cpl_msg_info(cpl_func,"Frame zeropoint uncertainty = %f mag", dzeropoint);
    cpl_msg_info(cpl_func,"Number of stars used for zeropoint computation = %d",
                 nzeropoint);
    
    cpl_propertylist_append_double(qc_phot, "ESO QC ZPOINT", zeropoint);
    cpl_propertylist_set_comment(qc_phot, "ESO QC ZPOINT", "Frame zeropoint ");

    cpl_propertylist_append_double(qc_phot, "ESO QC ZPOINTRMS", dzeropoint);
    cpl_propertylist_set_comment(qc_phot, "ESO QC ZPOINTRMS", 
                                 "Uncertainty of frame zeropoint [mag]");

    cpl_propertylist_append_int(qc_phot, "ESO QC ZPOINT NSTARS", nzeropoint);
    cpl_propertylist_set_comment(qc_phot, "ESO QC ZPOINT NSTARS", 
                             "Number of stars used for zeropoint computation");
    
    double derived_ext_coeff, derived_ext_coeff_err;

    if (setting->filter_name != NULL) {
        cpl_msg_info(cpl_func,
                     "Computing extinction "
                     "(assuming zeropoint = %.3f +- %.3f mag)",
                     expected_zeropoint,
                     dexpected_zeropoint);

        cpl_msg_indent_more();

        if (nzeropoint > 0) {
            derived_ext_coeff = ext_coeff +
                (expected_zeropoint - zeropoint) / avg_airmass;
            /* Things are very correlated here
               (e.g. ext_coeff was used to compute zeropoint).
           
               The final error on the ext.coeff. depends only
               on the reference and computed zeropoints' errors. 
               The airmass is assumed errorless.

               We assume the 2 zeropoints' errors are not correlated and
               add in quadrature.

               The derived extinction's error does not suffer from the part
               of dzeropoint which was due to the error of the assumed 
               extinction.
            */
            derived_ext_coeff_err = sqrt(
                dexpected_zeropoint*dexpected_zeropoint +
                dzeropoint*dzeropoint) / avg_airmass - dext_coeff*dext_coeff;
    
            cpl_msg_info(cpl_func, "Atmospheric extinction = "
                         "%f +- %f mag/airmass", derived_ext_coeff,
                         derived_ext_coeff_err);
        }
        else {
            cpl_msg_warning(cpl_func, "Too few stars available, "
                            "setting extinction to zero");
            derived_ext_coeff = 0;
            derived_ext_coeff_err = 9999;
        }
    }
    else {
        derived_ext_coeff = 0;
        derived_ext_coeff_err = 9999;
    }

    cpl_msg_info(cpl_func,"Atmospheric extinction = %f mag/airmass", 
                 derived_ext_coeff);
    cpl_msg_info(cpl_func,"Error in atmospheric extinction = %f mag/airmass", 
                 derived_ext_coeff_err);

    cpl_propertylist_append_double(qc_phot, "ESO QC EXTCOEFF",
                                   derived_ext_coeff);
    cpl_propertylist_set_comment(qc_phot, "ESO QC EXTCOEFF", 
                                 "Atmospheric extinction [mag/airmass]");

    cpl_propertylist_append_double(qc_phot, "ESO QC EXTCOEFFERR", 
                                   derived_ext_coeff_err);
    cpl_propertylist_set_comment(qc_phot, "ESO QC EXTCOEFFERR", 
                              "Error on atmospheric extinction [mag/airmass]");

    cpl_propertylist_append_double(qc_sources, "ESO QC ZPOINT NSRCEXTRACT", 
                                   cpl_table_get_nrow(sources));
    cpl_propertylist_set_comment(qc_sources, "ESO QC ZPOINT NSRCEXTRACT", 
                           "Number of sources extracted from the zpoint image");

    cpl_msg_indent_less();


    /* Convert to CPL table */
    aligned_phot = fors_create_sources_table(stars);
    assure( !cpl_error_get_code(), return,
            "Failed to create aligned photometry table");

    /* Save products */
    /* FIXME: FAP: use WCS corrected header */
    fors_dfs_save_table(frames, sources, SOURCES_STD,
                        qc_sources, parameters, fors_zeropoint_name, 
                        cpl_frameset_get_position(std_frame, 0));
    assure( !cpl_error_get_code(), return, "Saving %s failed",
            SOURCES_STD);
    cpl_propertylist_delete(qc_sources); qc_sources = NULL;

    /* Add keywords necessary for fors_photometry 
       (just reuse/overload to the qc propertylist variable)
    */
    cpl_propertylist_update_double(qc_phot, FORS_PFITS_EXPOSURE_TIME, 
                                   setting->exposure_time);
    cpl_propertylist_update_double(qc_phot, "AIRMASS", avg_airmass);
    
    cpl_table_fill_invalid_int(aligned_phot, "USE_CAT", 2);
    /* FIXME: FAP: use WCS corrected header */
    fors_dfs_save_table(frames, aligned_phot, ALIGNED_PHOT,
                        qc_phot, parameters, fors_zeropoint_name, 
                        cpl_frameset_get_position(std_frame, 0));
    assure( !cpl_error_get_code(), return, "Saving %s failed",
            ALIGNED_PHOT);
    
    cpl_propertylist_delete(qc_phot); qc_phot = NULL;

    /* FIXME: FAP: use WCS corrected header */
    fors_dfs_add_wcs(product_header,
                     cpl_frameset_get_position(std_frame, 0), setting);
    /* FIXME: FAP: use WCS corrected header */
    fors_dfs_add_exptime(product_header, 
                         cpl_frameset_get_position(std_frame, 0), 0.);

    cpl_propertylist_update_double(product_header, "AIRMASS", avg_airmass);
        
    dfs_save_image(frames, background, PHOT_BACKGROUND_STD_IMG,
                   product_header, parameters, fors_zeropoint_name, 
                   setting->version);
    assure( !cpl_error_get_code(), return, "Saving %s failed",
            PHOT_BACKGROUND_STD_IMG);
        
    cpl_image_delete(background); background = NULL;
    
    /* FIXME: FAP: use WCS corrected header */
    fors_dfs_save_image_err(frames, std, STANDARD_REDUCED_IMG,
                        product_header, NULL, parameters, fors_zeropoint_name, 
                        cpl_frameset_get_position(std_frame, 0));
    assure( !cpl_error_get_code(), return, "Saving %s failed",
            STANDARD_REDUCED_IMG);

    if(histogram != NULL)
    {
        dfs_save_image(frames, histogram, OFFSET_HISTOGRAM,
                NULL, parameters, fors_zeropoint_name, 
                setting->version);
        assure( !cpl_error_get_code(), return, "Saving %s failed",
                OFFSET_HISTOGRAM);

        cpl_image_delete(histogram); histogram = NULL;
    }
    else
    {
        cpl_msg_info(cpl_func,"Offset histogram not computed, not saving");
    }
        
    if (setting->filter_name == NULL) {

        /* No debug image can be created */

        cleanup;
        return;
    }

    /* Create debugging image

       Legend:

        ------  (horiz. line):   detected source

          |
          |    (vert. line):   catalog position
          |

          _
         / \     (circle) :   identified
         \_/
    */

    double color = fors_image_get_min(std);
    if (stars != NULL)
    {
        fors_star *s;
        for (s = fors_star_list_first(stars);
             s != NULL; 
             s = fors_star_list_next(stars)) {
            fors_image_draw(std, 0,
                            s->pixel->x,
                            s->pixel->y,
                            10, color);

            if (s->id != NULL && s->id->trusted) {
                fors_image_draw(std, 2,
                                s->pixel->x,
                                s->pixel->y,
                                10, color);
            }
        }
    }
    if (cat != NULL)
    {
        fors_std_star *s;
        for (s = fors_std_star_list_first(cat);
             s != NULL; 
             s = fors_std_star_list_next(cat))
        {
            if (s->trusted)
                fors_image_draw(std, 1,
                            s->pixel->x,
                            s->pixel->y,
                            10, color);
        }
        /* FIXME: FAP: use WCS corrected header */
        fors_dfs_save_image_err(frames, std, "DEBUG",
                            product_header, NULL, parameters, fors_zeropoint_name, 
                            cpl_frameset_get_position(std_frame, 0));
        assure( !cpl_error_get_code(), return, "Saving %s failed",
                "DEBUG");
    }

    cleanup;
    return;
}

/**
 * @brief  determine if zeropoint is inside cuts
 * @param  s      star
 * @param  data   hi and lo cuts
 * @return true iff the star's zeropoint is inside 
 *         the given intervals
 * 
 */
static bool
zeropoint_inside(const fors_star *s,
                 void *data) 
{
    struct zeropoint_inside_cuts{
        double hi, lo;   /* magnitude */
        double z, kappa; /* avg zeropoint, kappa */
    };
    zeropoint_inside_cuts * cuts = (zeropoint_inside_cuts *)data;
    
    double z  = fors_star_get_zeropoint(s, NULL);
    double dz = fors_star_get_zeropoint_err(s, NULL);

    return
        (cuts->lo                 <= z && z <= cuts->hi) ||
        (cuts->z - cuts->kappa*dz <= z && z <= cuts->z + cuts->kappa*dz);
}

#undef cleanup
#define cleanup \
do { \
    fors_star_list_delete(&subset, fors_star_delete); \
    fors_star_list_delete(&identified, fors_star_delete); \
} while(0)
/**
 * @brief   Compute zeropoint
 * @param   stars       list of stars, flags are set to 1 iff star is
 *                      used in final zeropoint computation
 * @param   cutoffE     rejection parameter (magnitude)
 * @param   cutoffk     rejection parameter (magnitude)
 * @param   dext_coeff  extinction coefficient error
 * @param   dcolor_term color coefficient error
 * @param   dzeropoint  (output) zeropoint stdev
 * @param   n           (output) number of stars used for zeropoint computation
 * @return  median zeropoint after rejection of negative outliers
 */
static double
get_zeropoint(fors_star_list *stars, 
              double cutoffE,
              double cutoffk,
              double dext_coeff,
              double dcolor_term,
              double avg_airmass,
              double *dzeropoint,
              int *n)
{
    fors_star_list *subset = NULL;
    fors_star_list *identified = 
        fors_star_list_extract(stars, fors_star_duplicate,
                               fors_star_is_identified, NULL);

    assure( stars != NULL, return 0, NULL );
    assure( dzeropoint != NULL, return 0, NULL );
    assure( n != NULL, return 0, NULL );

    if ( fors_star_list_size(identified) == 0 ) {
        cpl_msg_warning(cpl_func, 
                        "No identified stars for zeropoint computation");
        *n = 0;
        *dzeropoint = 0;
        cleanup;
        return 0;
    }
    
    cpl_msg_info(cpl_func, "Computing zeropoint (assuming extinction)");
    cpl_msg_indent_more();

    double zeropoint;
    double red_chisq = -1.0;

    /* This method does not take into account that the error bars are
       correlated, and therefore computes an unrealistically low
       dzeropoint */
    zeropoint = fors_star_list_mean_optimal(identified,
                                            fors_star_get_zeropoint, NULL,
                                            fors_star_get_zeropoint_err, NULL,
                                            dzeropoint,
                                            fors_star_list_size(identified) >= 2 ? &red_chisq : NULL);

    cpl_msg_info(cpl_func, "Optimal zeropoint (no rejection, %d stars) = %f mag",
                 fors_star_list_size(identified), zeropoint);  
    
    /* Reject stars that are absolute (0.3 mag) outliers and
       kappa sigma outliers. For robustness (against error in the initial
       zeropoint estimates) apply the absolute cut in two steps
       and update the estimated zeropoint after the first step.
    */
    struct {
        double hi, lo;   /* magnitude */
        double z, kappa; /* avg zeropoint, kappa */
    } cuts;
    cuts.hi = zeropoint + 5*cutoffE;
    cuts.lo = zeropoint - 5*cutoffE;
    cuts.z = zeropoint;
    cuts.kappa = cutoffk;
    
    subset = fors_star_list_extract(identified, fors_star_duplicate,
                                    zeropoint_inside, &cuts);


    if ( fors_star_list_size(subset) == 0 ) {
        cpl_msg_warning(cpl_func, 
                        "All stars rejected (%f mag). Cannot "
                        "compute zeropoint", 5*cutoffE);
        *n = 0;
        *dzeropoint = 0;
        cleanup;
        return 0;
    }

    zeropoint = fors_star_list_mean_optimal(subset,
                                            fors_star_get_zeropoint, NULL,
                                            fors_star_get_zeropoint_err, NULL,
                                            dzeropoint,
                                            fors_star_list_size(subset) >= 2 ? &red_chisq : NULL);

    cpl_msg_debug(cpl_func, "Optimal zeropoint (%.2f mag, %.2f sigma rejection) = %f mag",
                  5*cutoffE, cutoffk, zeropoint);  
    
    cuts.hi = zeropoint + cutoffE;
    cuts.lo = zeropoint - cutoffE;
    cuts.z = zeropoint;
    cuts.kappa = cutoffk;
    
    {
        fors_star_list *tmp = fors_star_list_duplicate(subset, fors_star_duplicate);
        fors_star_list_delete(&subset, fors_star_delete);

        subset = fors_star_list_extract(tmp, fors_star_duplicate,
                                        zeropoint_inside, &cuts);

        if ( fors_star_list_size(subset) == 0 ) {
            cpl_msg_warning(cpl_func, 
                            "All stars rejected (%f mag, %f sigma). Cannot "
                            "compute zeropoint", cutoffE, cutoffk);
            *n = 0;
            *dzeropoint = 0;
            cleanup;
            return 0;
        }

        fors_star_list_delete(&tmp, fors_star_delete);
    }

    zeropoint = fors_star_list_mean_optimal(subset,
                                            fors_star_get_zeropoint, NULL,
                                            fors_star_get_zeropoint_err, NULL,
                                            dzeropoint,
                                            fors_star_list_size(subset) >= 2 ? &red_chisq : NULL);
    
    cpl_msg_info(cpl_func, "Optimal zeropoint (%.2f mag, %.2f sigma rejection) = %f mag",
                 cutoffE, cutoffk, zeropoint);  
    
    *n = fors_star_list_size(subset);
    {
        int outliers = 
            fors_star_list_size(identified) - fors_star_list_size(subset);
        cpl_msg_info(cpl_func, "%d outlier%s rejected, %d non-outliers",
                     outliers, outliers == 1 ? "" : "s",
                     *n);
    }

    if ( *n == 0 ) {
        cpl_msg_warning(cpl_func, 
                        "All stars were rejected during zeropoint computation" );
        *dzeropoint = 0;
        cleanup;
        return 0;
    }







    /*
      Build zeropoint covariance matrix.
      We have already the variances from fors_star_get_zeropoint_err().
      Non-diagonal terms are
         Cij = airmass^2 * Variance(ext.coeff) + color_i * color_j * Variance(color.coeff)

      It was considered and tried to subtract the term
               airmass^2 * Variance(ext.coeff)
      from every Cij. This has no effect on the relative weights, only the weight's overall
      normalization. Since we use the normalization for computing the zeropoint error this
      term is kept.

    */
    cpl_matrix *covariance = cpl_matrix_new(*n,
                                            *n);

    /* Duplicate the list to allow simultaneous iterations */
    fors_star_list *ident_dup = fors_star_list_duplicate(subset, fors_star_duplicate);
    {
      
      fors_star *s, *t;
      int i, j;
      for (s = fors_star_list_first(subset), i = 0;
           s != NULL;
           s = fors_star_list_next(subset), i++) {

        for (t = fors_star_list_first(ident_dup), j = 0;
             t != NULL;
             t = fors_star_list_next(ident_dup), j++) {
          
          double cij;

          if (fors_star_equal(s, t)) {
              cij = fors_star_get_zeropoint_err(s, NULL)*fors_star_get_zeropoint_err(s, NULL);
              /*  -avg_airmass*avg_airmass*dext_coeff*dext_coeff */
          }
          else {
              cij = s->id->color * t->id->color * dcolor_term*dcolor_term
                  + avg_airmass*avg_airmass*dext_coeff*dext_coeff;
          }
          
          cpl_matrix_set(covariance, i, j, cij);
        }
      }
    }
    /* cpl_matrix_dump(covariance, stdout); */
    /* cpl_matrix_dump(cpl_matrix_invert_create(covariance), stdout); */

    /*
      Compute optimal weights, w, as
      
      w = C^-1 * const

      C    : nxn covariance matrix
      const: nx1 constant vector with all elements equal to 1
     */

    cpl_matrix *covariance_inverse = cpl_matrix_invert_create(covariance);

    assure( !cpl_error_get_code(), return 0,
            "Could not invert zeropoints covariance matrix");

    /*  cpl_matrix_dump(cpl_matrix_product_create(covariance_inverse, covariance), stdout); */

    /* fprintf(stderr, "is_identity = %d\n", cpl_matrix_is_identity(cpl_matrix_product_create(covariance_inverse, covariance),1e-10)); */

    cpl_matrix_delete(covariance); covariance = NULL;

    cpl_matrix *const_vector = cpl_matrix_new(*n, 1);
    cpl_matrix_fill(const_vector, 1.0);
    
    cpl_matrix *weights = cpl_matrix_product_create(covariance_inverse, const_vector);

    cpl_matrix_delete(const_vector); const_vector = NULL;

    /* cpl_matrix_dump(weights, stdout); */
    
    
    {
        double wz = 0;
        double w = 0;
        
        int i;
        fors_star *s;
        for (i = 0, s = fors_star_list_first(subset);
             s != NULL;
             s = fors_star_list_next(subset), i++) {
            
            double weight = cpl_matrix_get(weights, i, 0);

            cpl_msg_debug(cpl_func, "Weight_%d = %f", i, weight);
            
            wz += weight * fors_star_get_zeropoint(s, NULL);
            w += weight;

            /* Loop through original list to record the weight of this star */
            {
                fors_star *t;
                
                for (t = fors_star_list_first(stars);
                     t != NULL;
                     t = fors_star_list_next(stars)) {
                    
                    if (fors_star_equal(s, t)) {
                        t->weight = weight;
                    }
                }
            }
        }

        cpl_matrix_delete(weights); weights = NULL;

        cpl_msg_debug(cpl_func, "Sum of weights = %f", w);

        /* 
           C is positive definite (because all eigenvalues are positive). Therefore
           C^-1 is also positive definite, a property of positive definite matrices.
           
           Positive definite matrices always have the property that
                 z* C z > 0     for any non-zero (complex) vector z

           The sum of the weights is just
                const.* w = const.* C^-1 const.
           where const. is our constant vector filled with 1. Therefore the sum of
           the weights should always be positive. But make the paranoia check anyway:
         */

        assure( w != 0, return 0, "Sum of optimal weights is zero!" );
        assure( sqrt(w) != 0, return 0, "Square root of sum of optimal weights is zero!" );
        
        zeropoint = wz / w;
        *dzeropoint = 1 / sqrt(w);
    }

    /* Previous code: weighted average, uncorrelated errors

    zeropoint = fors_star_list_mean_optimal(
        subset, 
        fors_star_get_zeropoint, NULL,
        fors_star_get_zeropoint_err, NULL,
        dzeropoint,
        *n >= 2 ? &red_chisq : NULL);
    */
    
    cpl_msg_info(cpl_func, "Optimal zeropoint = %f +- %f mag", 
                 zeropoint, *dzeropoint);

    if (*n >= 2) {
        fors_star *s, *t;
        int i, j;

        red_chisq = 0;

        for (s = fors_star_list_first(subset), i = 0;
             s != NULL;
             s = fors_star_list_next(subset), i++) {
            
            for (t = fors_star_list_first(ident_dup), j = 0;
                 t != NULL;
                 t = fors_star_list_next(ident_dup), j++) {
                
                red_chisq += 
                    (fors_star_get_zeropoint(s, NULL) - zeropoint) *
                    (fors_star_get_zeropoint(t, NULL) - zeropoint) *
                    cpl_matrix_get(covariance_inverse, i, j);
            }
        }
        red_chisq /= (*n - 1);
    }

    cpl_matrix_delete(covariance_inverse); covariance_inverse = NULL;

    fors_star_list_delete(&ident_dup, fors_star_delete);
    
    cpl_msg_info(cpl_func, "Reduced chi square = %f", red_chisq);
    
    cpl_msg_indent_less();

    cleanup;
    return zeropoint;
}

#undef cleanup
#define cleanup \
do { \
    fors_std_star_list_delete(&internal_cat, fors_std_star_delete); \
} while (0)
/**
 * @brief   Load standard star catalogue(s) and do the astrometry.
 * @param   std_cat_frames  Standard star catalogue frames
 * @param   filter_band     Standard star catalogue import: optical filter band
 * @param   color_correct   Standard star catalogue import: color correction
 *                          term
 * @param   dcolor_correct  Standard star catalogue import: 1-sigma error of
 *                          color correction term
 * @param   id_method       FORS pipeline pattern identification method
 * @param   extracted       Extracted sources (modified)
 * @param   wcs_header      WCS header (modified)
 * @param   std_cat         (Optional) output standard star catalogue,
 *                          also provided in some error cases,
 *                          can be NULL
 * @return  CPL error code
 * 
 * @par Output Details:
 * - If a standard star catalogue could be loaded, it will be returned also
 *   in the case of error. This means, that it must always be deleted.
 */
static cpl_error_code
fors_zeropoint_astrometry(                  const cpl_frameset  *std_cat_frames,
                                            char                filter_band,
                                            double              color_correct,
                                            double              dcolor_correct,
                                            const identify_method
                                                                *id_method,
                                            fors_star_list      *extracted,
                                            cpl_propertylist    *wcs_header,
                                            fors_std_star_list  **std_cat,
                                            cpl_image           **histogram)
{
    double              dcrpix_x = 0,
                        dcrpix_y = 0;
    fors_std_star_list  *internal_cat = NULL,
                        *used_cat;
    cpl_errorstate      errstat = cpl_errorstate_get();

    
    if (std_cat != NULL)
        *std_cat = NULL;
    
    cassure_automsg(                        std_cat_frames != NULL,
                                            CPL_ERROR_NULL_INPUT,
                                            return cpl_error_get_code());
    cassure_automsg(                        id_method != NULL,
                                            CPL_ERROR_NULL_INPUT,
                                            return cpl_error_get_code());
    cassure_automsg(                        extracted != NULL,
                                            CPL_ERROR_NULL_INPUT,
                                            return cpl_error_get_code());
    cassure_automsg(                        wcs_header != NULL,
                                            CPL_ERROR_NULL_INPUT,
                                            return cpl_error_get_code());

    cpl_msg_info(cpl_func, "Loading standard star catalogue(s):");
    cpl_msg_indent_more();
    /*
    used_cat = internal_cat = fors_std_cat_load_old(
                                            std_cat_frames,
                                            filter_band,
                                            color_term,
                                            dcolor_term);
    */
    used_cat = internal_cat = fors_std_cat_load(
                                            std_cat_frames,
                                            filter_band,
                                            0,  /* don't require all frames */
                                            color_correct,
                                            dcolor_correct);
    cpl_msg_indent_less();
    assure(                                 cpl_errorstate_is_equal(errstat),
                                            return cpl_error_get_code(),
                                            "Std catalogue loading failed");
    
    /* keep catalogue for output if desired */
    if (std_cat != NULL)
    {
        *std_cat = used_cat;
        internal_cat = NULL;
    }
    if (0) if (used_cat) fors_std_star_print_list(CPL_MSG_DEBUG, used_cat);

    /* get (x,y) std star positions */
    fors_std_star_list_apply_wcs(           used_cat, wcs_header);
    assure(                                 cpl_errorstate_is_equal(errstat),
                                            return cpl_error_get_code(),
                                            "Failed to apply WCS to catalogue");

    /* Identify (std stars are duplicated and linked to stars) */
    fors_identify(extracted, used_cat, id_method, histogram);
    assure(                                 cpl_errorstate_is_equal(errstat),
                                            return cpl_error_get_code(),
                                            "Failed to identify sources");

    /* correct the wcs */
    fors_zeropoint_astrometry_get_wcs_shift_px(
                                            extracted,
                                            &dcrpix_x,
                                            &dcrpix_y);
    assure(                                 cpl_errorstate_is_equal(errstat),
                                            return cpl_error_get_code(),
                                            "Failed to determine WCS correction"
                                            );

    cpl_msg_info(                           cpl_func,
                                            "Correcting the WCS origin by "
                                            "(%f, %f)",
                                            -dcrpix_x,
                                            -dcrpix_y);

    fors_zeropoint_astrometry_shift_wcs_origin(
                                            wcs_header,
                                            -dcrpix_x,
                                            -dcrpix_y);
    assure(                                 cpl_errorstate_is_equal(errstat),
                                            return cpl_error_get_code(),
                                            "Failed to correct WCS origin");

    /* re-apply (x,y) std star positions */
    {
        /* create a list of the identified std stars */
        fors_star   *s;
        fors_std_star_list  *identified;
        identified = fors_std_star_list_new();

        for (   s = fors_star_list_first(extracted);
                s != NULL;
                s = fors_star_list_next(extracted))
        {
            if (s->id != NULL)
                fors_std_star_list_insert(  identified,
                                            (fors_std_star*)s->id); /* !const */
        }

        if (fors_std_star_list_size(identified) > 0)
            fors_std_star_list_apply_wcs(identified, wcs_header);

        /* delete the list object but not the std stars */
        fors_std_star_list_delete(&identified, NULL);
    }
    assure(                                 cpl_errorstate_is_equal(errstat),
                                            return cpl_error_get_code(),
                                            "Failed to correct std (x,y)");

    /* ATTENTION: the following code links fors_std_star* objects to
     * the stars, which represent non-std stars (trusted-flag=false).
     * make sure later invoked functions respect this flag!
     *
     * Non-std stars are created and linked to stars.
     * Btw., those stars can be any sextracted celestial object. */
    cpl_msg_info(cpl_func, "Assigning RA & DEC to non-standard stars");
    cpl_msg_indent_more();
    fors_zeropoint_astrometry_apply_unidentified_xy2radec(
                                            extracted,
                                            wcs_header);
    cpl_msg_indent_less();
    assure(                                 cpl_errorstate_is_equal(errstat),
                                            return cpl_error_get_code(),
                                            "Failed to apply WCS to non-"
                                            "identified stars");

    cleanup;
    return (    cpl_errorstate_is_equal(errstat) ?
                CPL_ERROR_NONE :
                cpl_error_get_code());
}

#undef cleanup
#define cleanup \
do { \
    cpl_vector_delete(vdx_long); vdx_long = NULL; \
    cpl_vector_delete(vdy_long); vdy_long = NULL; \
    cpl_vector_unwrap(vdx_only_std); vdx_only_std = NULL; \
    cpl_vector_unwrap(vdy_only_std); vdy_only_std = NULL; \
} while (0)
/**
 * @brief   Determine the median difference std_star.pixel - star.pixel.
 * @param   stars   Star list contain (at least partially) pointers to std stars
 * @param   dx      (Output) median x difference
 * @param   dy      (Output) median y difference
 * @return  CPL error code
 */
static cpl_error_code
fors_zeropoint_astrometry_get_wcs_shift_px( const fors_star_list    *stars,
                                            double                  *dx,
                                            double                  *dy)
{
    const fors_star *s;
    int             n_stars,
                    n_std_stars = 0;
    cpl_vector      *vdx_long = NULL,
                    *vdx_only_std = NULL,
                    *vdy_long = NULL,
                    *vdy_only_std = NULL;
    cpl_errorstate  errstat = cpl_errorstate_get();
    
    /* init output */
    if (dx != NULL)
        *dx = 0;
    if (dy != NULL)
        *dy = 0;
    
    cassure_automsg(                        stars != NULL,
                                            CPL_ERROR_NULL_INPUT,
                                            return cpl_error_get_code());
    cassure_automsg(                        dx != NULL,
                                            CPL_ERROR_NULL_INPUT,
                                            return cpl_error_get_code());
    cassure_automsg(                        dy != NULL,
                                            CPL_ERROR_NULL_INPUT,
                                            return cpl_error_get_code());
    
    n_stars = fors_star_list_size(stars);
    
    cassure_automsg(                        n_stars > 0,
                                            CPL_ERROR_DATA_NOT_FOUND,
                                            return cpl_error_get_code());
    
    vdx_long = cpl_vector_new(n_stars);
    vdy_long = cpl_vector_new(n_stars);
    
    for (   s = fors_star_list_first_const(stars);
            s != NULL;
            s = fors_star_list_next_const(stars))
    {
        const fors_std_star *id = s->id;
        if (id != NULL)
        {
            cpl_vector_set(vdx_long, n_std_stars, id->pixel->x - s->pixel->x);
            cpl_vector_set(vdy_long, n_std_stars, id->pixel->y - s->pixel->y);
            n_std_stars++;
        }
    }
    
    passure(                                cpl_errorstate_is_equal(errstat),
                                            return cpl_error_get_code());
    cassure_automsg(                        n_std_stars > 0,
                                            CPL_ERROR_DATA_NOT_FOUND,
                                            return cpl_error_get_code());
    
    vdx_only_std = cpl_vector_wrap(n_std_stars, cpl_vector_get_data(vdx_long));
    vdy_only_std = cpl_vector_wrap(n_std_stars, cpl_vector_get_data(vdy_long));
    
    *dx = cpl_vector_get_median(vdx_only_std);
    *dy = cpl_vector_get_median(vdy_only_std);
    
    passure(                                cpl_errorstate_is_equal(errstat),
                                            return cpl_error_get_code());
    
    cleanup;
    
    return CPL_ERROR_NONE;
}

#undef cleanup
#define cleanup
/**
 * @brief   Shift the origin of the WCS in a 2-dimensional frame header.
 * @param   header  Frame header
 * @param   dx      X shift in pixels
 * @param   dy      Y shift in pixels
 * @return  CPL error code
 */
static cpl_error_code
fors_zeropoint_astrometry_shift_wcs_origin( cpl_propertylist    *header,
                                            double  dx,
                                            double  dy)
{
    double          crpix_x,
                    crpix_y;
    cpl_errorstate  errstat = cpl_errorstate_get();
    
    cassure_automsg(                        header != NULL,
                                            CPL_ERROR_NULL_INPUT,
                                            return cpl_error_get_code());
    
    /* FIXME: wanted: creating wcs from header, modifying wcs, storing
     * back wcs into header. This was not possible due to the lack
     * of a function to store a wcs in a property list. */
    
    if (!cpl_propertylist_has(header, "CRPIX1"))
    {
        cpl_error_set_message(              cpl_func,
                                            CPL_ERROR_DATA_NOT_FOUND,
                                            "no WCS found in header");
        cleanup;
        return cpl_error_get_code();
    }
    if (!cpl_propertylist_has(header, "CRPIX2")
        || cpl_propertylist_has(header, "CRPIX3"))
    {
        cpl_error_set_message(              cpl_func,
                                            CPL_ERROR_INCOMPATIBLE_INPUT,
                                            "WCS in header is not "
                                            "2-dimensional");
        cleanup;
        return cpl_error_get_code();
    }
    
    
    crpix_x = cpl_propertylist_get_double(header, "CRPIX1");
    crpix_y = cpl_propertylist_get_double(header, "CRPIX2");
    assure(                                 cpl_errorstate_is_equal(errstat),
                                            return cpl_error_get_code(),
                                            "CRPIXn keywords must be of type "
                                            "double");
    crpix_x += dx;
    crpix_y += dy;
    cpl_propertylist_set_double(header, "CRPIX1", crpix_x);
    cpl_propertylist_set_double(header, "CRPIX2", crpix_y);
    
    return (    cpl_errorstate_is_equal(errstat) ?
                CPL_ERROR_NONE :
                cpl_error_get_code());
}

#undef cleanup
#define cleanup \
do { \
    cpl_wcs_delete(wcs); wcs = NULL; \
    cpl_matrix_delete(mradec); mradec = NULL; \
    cpl_matrix_delete(mxy); mxy = NULL; \
    cpl_array_delete(wcs_conversion_status); wcs_conversion_status = NULL; \
    fors_std_star_delete(&unknown); \
} while (0)
/**
 * @brief   Create non-standard stars (using the fors_std_star object) connected to the star list entries and assign RA & DEC.
 * @param   stars   Star list
 * @param   header  Frame header containing WCS
 * @return  Non-standard star list, NULL in the case of error
 * 
 * This means, that at the end for every star from the star-list the referenced
 * standard-star-object should be deleted, then the existing standard-star-list
 * object without deleting the entries, and finally the star-list-object.
 */
static cpl_error_code
fors_zeropoint_astrometry_apply_unidentified_xy2radec(
                                            fors_star_list          *stars,
                                            const cpl_propertylist  *header)
{
    cpl_wcs             *wcs = NULL;
    cpl_matrix          *mradec = NULL,
                        *mxy = NULL;
    cpl_array           *wcs_conversion_status = NULL;
    fors_star           *star = NULL;
    fors_std_star       *unknown = NULL;
    int                 n_unknowns = 0,
                        n_wcs_successes = 0,
                        n;
    int                 *sdat;
    union _nant {
        unsigned char   bytes[8];
        double          val;
    } nan;
    cpl_errorstate      errstat = cpl_errorstate_get();
    
    cassure_automsg(                        stars != NULL,
                                            CPL_ERROR_NULL_INPUT,
                                            return cpl_error_get_code());
    cassure_automsg(                        header != NULL,
                                            CPL_ERROR_NULL_INPUT,
                                            return cpl_error_get_code());
    
    wcs = cpl_wcs_new_from_propertylist(    header);
    assure(cpl_errorstate_is_equal(errstat), return cpl_error_get_code(), NULL);
    
    {
        /* create NaN */
        int nn;
        for (nn = 0; nn < 8; nn++)
            nan.bytes[nn] = (unsigned char)255;
    }
    
    /* assuming that cpl_wcs_convert has coordinates in rows */
    /* count unknowns */
    for (   star = fors_star_list_first(stars);
            star != NULL;
            star = fors_star_list_next(stars))
    {
        if (star->id == NULL)
            n_unknowns++;
    }
    passure(cpl_errorstate_is_equal(errstat), return cpl_error_get_code());

    if (n_unknowns == 0)
        return cpl_error_get_code();
            
    mxy = cpl_matrix_new(n_unknowns, 2);
    passure(cpl_errorstate_is_equal(errstat), return cpl_error_get_code());
    
    /* copy x,y */
    for (   star = fors_star_list_first(stars), n = 0;
            star != NULL;
            star = fors_star_list_next(stars))
    {
        if (star->id == NULL)
        {
            cpl_matrix_set(mxy, n, 0, star->pixel->x);
            cpl_matrix_set(mxy, n, 1, star->pixel->y);
            n++;
        }
    }
    passure(cpl_errorstate_is_equal(errstat), return cpl_error_get_code());
    
    cpl_wcs_convert(                        wcs,
                                            mxy,
                                            &mradec,
                                            &wcs_conversion_status,
                                            CPL_WCS_PHYS2WORLD);
    assure(cpl_errorstate_is_equal(errstat), return cpl_error_get_code(), NULL);
    cpl_matrix_delete(mxy); mxy = NULL;
    
    passure(n_unknowns == cpl_array_get_size(wcs_conversion_status),
            return cpl_error_get_code());
    
    /* FIXME: we have to operate on the data array of wcs_conversion_status,
     * because currently cpl_wcs_convert() does the same and thus ignores
     * the "valid" flags */
    sdat = cpl_array_get_data_int(wcs_conversion_status);
    
    for (   star = fors_star_list_first(stars), n = 0;
            star != NULL;
            star = fors_star_list_next(stars))
    {
        if (star->id == NULL)
        {
            if (sdat[n] == 0)
            {
                double  ra,
                        dec;
                n_wcs_successes++;
                
                ra = cpl_matrix_get(mradec, n, 0);
                dec = cpl_matrix_get(mradec, n, 1);
                unknown = fors_std_star_new(ra, dec,
                                            nan.val, nan.val,
                                            nan.val, nan.val,
                                            nan.val, nan.val,
                                            nan.val,
                                            NULL);  /* no name */
                passure(                    cpl_errorstate_is_equal(errstat),
                                            return cpl_error_get_code());
                
                unknown->trusted = false;   /* this star has no catalogue mag*/
                unknown->pixel->x = star->pixel->x;
                unknown->pixel->y = star->pixel->y;
                
                star->id = unknown;
                unknown = NULL;
            }
            n++;
        }
    }
    
    cpl_msg_info(                           cpl_func,
                                            "Assigned RA & DEC to %d unknown "
                                            "stars",
                                            n_wcs_successes);
    if (n_wcs_successes < n_unknowns)
        cpl_msg_warning(                    cpl_func,
                                            "%d WCS conversions failed",
                                            n_unknowns - n_wcs_successes);
    
    cleanup;
    
    return (    cpl_errorstate_is_equal(errstat) ?
                CPL_ERROR_NONE :
                cpl_error_get_code());
}

#undef cleanup
#define cleanup
/**
 * @brief   Dump one error state of the error history.
 * @param   self
 * @param   first
 * @param   last
 *
 */
void
fors_zeropoint_errorstate_dump_as_warning(  unsigned self,
                                            unsigned first,
                                            unsigned last)
{
    const cpl_boolean is_reverse = first > last ? CPL_TRUE : CPL_FALSE;
    const unsigned    newest     = is_reverse ? first : last;
    /*const unsigned    oldest     = is_reverse ? last : first;*/
    self = self;
    first = first;
    last = last;
    /*assert( oldest <= self );
    assert( newest >= self );*/
    
    if (newest == 0)
    {
        /*assert( oldest == 0);*/
        cpl_msg_info(cpl_func, "Success");
    }
    else
    {
        /*assert( oldest > 0);*/
        cpl_msg_warning(                    cpl_func,
                                            "- %s (%s(), %s: %d)",
                                            cpl_error_get_message(),
                                            cpl_error_get_function(),
                                            cpl_error_get_file(),
                                            cpl_error_get_line());
        /*if (self == last)
            cpl_msg_indent_less();*/
    }
}

/**@}*/
