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

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

#include <math.h>

#include <cxmessages.h>
#include <cxmemory.h>

#include <cpl_recipe.h>
#include <cpl_plugininfo.h>
#include <cpl_parameterlist.h>
#include <cpl_frameset.h>
#include <cpl_propertylist.h>
#include <cpl_vector.h>
#include <cpl_msg.h>

#include "gialias.h"
#include "gierror.h"
#include "giarray.h"
#include "giframe.h"
#include "giimage.h"
#include "giwindow.h"
#include "gifibers.h"
#include "gibias.h"
#include "gimath.h"
#include "gistacking.h"
#include "giqclog.h"
#include "giutils.h"


#define GIMASTERBIAS_BIAS_EXTENSION_IMG 0
#define GIMASTERBIAS_BIAS_EXTENSION_PL 0
#define GIMASTERBIAS_BAD_PIXEL_EXTENSION 0


struct GiMasterbiasConfig {

    cxbool removeoverscan;
    cxbool correctbadpixels;

    struct {
        cxbool create;
        cxdouble factor;
        cxdouble fraction;
    } bpm;
    cxbool crebadpixmap;
};

typedef struct GiMasterbiasConfig GiMasterbiasConfig;

static cxint gimasterbias(cpl_parameterlist*, cpl_frameset*);
static cxint giqcmasterbias(cpl_frameset*);


/*
 * Maximum fraction of pixels which may be bad.
 */

static cxdouble max_bpx_fraction = 0.15;


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

static cxint
gimasterbias_create(cpl_plugin* plugin)
{

    cpl_recipe* recipe = (cpl_recipe*)plugin;

    cpl_parameter* p;


    giraffe_error_init();


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

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

    /*
     * Fill the parameter list.
     */

    /* Stacking */

    giraffe_stacking_config_add(recipe->parameters);

    /* Masterbias */

    p = cpl_parameter_new_value("giraffe.masterbias.overscan.remove",
                                CPL_TYPE_BOOL,
                                "Remove pre- and over-scan regions from "
                                "the created master bias image.",
                                "giraffe.masterbias.overscan",
                                FALSE);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "mbias-oscremove");
    cpl_parameterlist_append(recipe->parameters, p);

    p = cpl_parameter_new_value("giraffe.masterbias.badpixel.clean",
                                CPL_TYPE_BOOL,
                                "Correct master bias image for bad pixels",
                                "giraffe.masterbias.badpixel",
                                FALSE);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "mbias-bpxclean");
    cpl_parameterlist_append(recipe->parameters, p);

    p = cpl_parameter_new_value("giraffe.masterbias.bpm.create",
                                CPL_TYPE_BOOL,
                                "Create bad pixel map using a simple "
                                "thresholding algorithm. (temporary!)",
                                "giraffe.masterbias.bpm",
                                TRUE);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "bpm-create");
    cpl_parameterlist_append(recipe->parameters, p);

    p = cpl_parameter_new_value("giraffe.masterbias.bpm.factor",
                                CPL_TYPE_DOUBLE,
                                "Readout noise multiplier defining the "
                                "valid range of pixel values for searching "
                                "bad pixels.",
                                "giraffe.masterbias.bpm",
                                5.0);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "bpm-factor");
    cpl_parameterlist_append(recipe->parameters, p);

    p = cpl_parameter_new_value("giraffe.masterbias.bpm.fraction",
                                CPL_TYPE_DOUBLE,
                                "Maximum fraction of pixels which may be "
                                "flagged as 'bad. If more pixels are "
                                "found to be 'bad a warning is issued.",
                                "giraffe.masterbias.bpm",
                                max_bpx_fraction);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "bpm-frac");
    cpl_parameterlist_append(recipe->parameters, p);

    return 0;

}

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

static cxint
gimasterbias_exec(cpl_plugin* plugin)
{

    cpl_recipe* recipe = (cpl_recipe*)plugin;

    cxint status = 0;


    if (recipe->parameters == NULL || recipe->frames == NULL) {
        return 1;
    }

    status = gimasterbias(recipe->parameters, recipe->frames);

    if (status != 0) {
        return 1;
    }

    status = giqcmasterbias(recipe->frames);

    if (status != 0) {
        return 1;
    }

    return 0;

}


static cxint
gimasterbias_destroy(cpl_plugin* plugin)
{

    cpl_recipe* recipe = (cpl_recipe*)plugin;


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

    cpl_parameterlist_delete(recipe->parameters);

    giraffe_error_clear();

    return 0;

}

/*
 * Remove bad pixels from a Image using a bad pixel map
 */

static cxint
gimasterbias_remove_badpixels(GiImage* img, GiImage* img_badpixels)
{

    const cxchar* const fctid = "gimasterbias_remove_badpixels";


    cxbool found_first;

    register cxint j;
    register cxint k;
    register cxint nr_pairs;
    register cxint d;
    register cxint sign;

    cxint ncol_bp;
    cxint nrow_bp;
    cxint badx;
    cxint bady;
    cxint cx;
    cxint cy;
    cxint search_horizon = 100;

    cxint* pi_bp  = NULL;

    cxint sx[] = { 0, 1, 1, 1 };
    cxint sy[] = { 1,-1, 0, 1 };

    cxlong npix_bp;
    cxlong nr_bad_pixels = 0L;
    cxlong n;

    cxdouble sumd;
    cxdouble save = 0.;

    cxdouble* pd_img = NULL;

    cxdouble estimate[4];



    if (!img) {
        cpl_msg_error(fctid, "NULL Image as input, aborting..." );
        return -1;
    }

    if (!img_badpixels) {
        cpl_msg_error(fctid, "NULL Bad Pixel Image as input, aborting..." );
        return -1;
    }

    ncol_bp = cpl_image_get_size_x(giraffe_image_get(img_badpixels));
    nrow_bp = cpl_image_get_size_y(giraffe_image_get(img_badpixels));
    npix_bp = ncol_bp * nrow_bp;
    pi_bp   = cpl_image_get_data_int(giraffe_image_get(img_badpixels));

    pd_img  = cpl_image_get_data_double(giraffe_image_get(img));

    for (n=0; n<npix_bp; n++) {
        if (pi_bp[n]!=0)
            nr_bad_pixels++;
    }

    if (((cxdouble)nr_bad_pixels / (cxdouble)npix_bp) >= max_bpx_fraction) {
        cpl_msg_error(fctid, "Too many bad pixels, aborting..." );
        return -1;
    }

    /************************************************************************
                                     PROCESSING
    ************************************************************************/

    for (badx=0; badx<ncol_bp; badx++) {
        for (bady=0; bady<nrow_bp; bady++) {
            if (pi_bp[badx + bady * ncol_bp]==1) {
                nr_pairs = 0;
                for (j=0; j<4; j++) {
                    estimate[nr_pairs] = 0.0;
                    sumd = 0.0;
                    found_first = FALSE;
                    for (k=0; k<2; k++) {
                        sign = 2 * k - 1;
                        d = 0;
                        cx = badx;
                        cy = bady;
                        do {
                            cx += sign * sx[j];
                            cy += sign * sy[j];
                            if (cx<0 || cx>=ncol_bp || cy<0 || cy>=nrow_bp)
                                break;
                            d++;
                        } while (pi_bp[cx+cy*ncol_bp] && (d<search_horizon));

                        if (cx>=0 && cx<ncol_bp && cy>=0 && cy<nrow_bp &&
                            (d<search_horizon)                             )
                        {
                            save = pd_img[cx+cy*ncol_bp];
                            estimate[nr_pairs] += save / d;
                            sumd += 1.0 / (cxdouble) d;
                            if (k) {
                                estimate[nr_pairs] /= sumd;
                                nr_pairs++;
                            } else {
                                found_first = TRUE;
                            }
                        } else {
                            if (k) {
                                if (found_first) {
                                    estimate[nr_pairs] = save;
                                    nr_pairs++;
                                    if (nr_pairs>2) {

                                        cpl_vector* _estimate =
                                            cpl_vector_wrap(nr_pairs,
                                                            estimate);

                                        pd_img[badx+bady*ncol_bp] =
                                            cpl_vector_get_median(_estimate);

                                        cpl_vector_unwrap(_estimate);
                                        _estimate = NULL;

                                    } else if (nr_pairs==2) {
                                        pd_img[badx+bady*ncol_bp] =
                                            (estimate[0]+estimate[1]) * 0.5;
                                    } else if (nr_pairs==1) {
                                        pd_img[badx+bady*ncol_bp] =
                                            estimate[0];
                                    } else {
                                        cpl_msg_warning(
                                            fctid,
                                            "Can't correct badpixel [%d,%d]",
                                            badx,
                                            bady
                                        );
                                    }
                                }
                            }
                        }
                    }

                }
            }
        }
    }

    return 0;

}


/*
 * The actual recipe starts here.
 */

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

    const cxchar* const fctid = "gimasterbias";


    cxint raw_bias_count  = 0;
    cxint bad_pixel_count = 0;
    cxint e_code          = 0;

    cxlong i = 0;
    cxlong j = 0;

    cpl_propertylist* properties = NULL;

    cpl_frame* curr_frame = NULL;
    cpl_frame* bad_pixel_frame = NULL;
    cpl_frame* product_frame = NULL;

    cpl_parameter* p = NULL;

    GiImage** raw_bias_list = NULL;
    GiImage* bad_pixels = NULL;
    GiImage* master_bias = NULL;

    GiMasterbiasConfig mbias_config;

    GiStackingConfig* stack_config = NULL;

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

    GiGroupInfo groups[] = {
        {GIFRAME_BIAS, CPL_FRAME_GROUP_RAW},
        {GIFRAME_BADPIXEL_MAP, CPL_FRAME_GROUP_CALIB},
        {NULL, CPL_FRAME_GROUP_NONE}
    };


    /************************************************************************
                                    PREPROCESSING
    ************************************************************************/

    p = cpl_parameterlist_find(config, "giraffe.masterbias.overscan.remove");
    mbias_config.removeoverscan = cpl_parameter_get_bool(p);

    p = cpl_parameterlist_find(config, "giraffe.masterbias.badpixel.clean");
    mbias_config.correctbadpixels = cpl_parameter_get_bool(p);

    p = cpl_parameterlist_find(config, "giraffe.masterbias.bpm.create");
    mbias_config.bpm.create = cpl_parameter_get_bool(p);

    p = cpl_parameterlist_find(config, "giraffe.masterbias.bpm.factor");
    mbias_config.bpm.factor = cpl_parameter_get_double(p);

    p = cpl_parameterlist_find(config, "giraffe.masterbias.bpm.fraction");
    mbias_config.bpm.fraction = cpl_parameter_get_double(p);


    e_code = giraffe_frameset_set_groups(set, groups);

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


    /************************************************************************
                                     PROCESSING
    ************************************************************************/

    stack_config = giraffe_stacking_config_create(config);

    /* check number of images */
    raw_bias_count = cpl_frameset_count_tags(set, GIFRAME_BIAS);

    if (raw_bias_count < stack_config->min_nr_frames) {
        cpl_msg_error(fctid, "Not enough raw bias Images [%d, need %d], "
                      "aborting...", raw_bias_count,
                      stack_config->min_nr_frames);

        giraffe_stacking_config_destroy(stack_config);
        stack_config = NULL;

        return -1;
    }

    bad_pixel_count = cpl_frameset_count_tags(set, GIFRAME_BADPIXEL_MAP);

    if (mbias_config.correctbadpixels == TRUE) {
        if (bad_pixel_count != 1) {
            cpl_msg_error(fctid, "Invalid number of bad pixel Images "
                          "[%d instead of 1], aborting...", bad_pixel_count);

            giraffe_stacking_config_destroy(stack_config);
            stack_config = NULL;

            return -1;
        }
    }

    cpl_msg_info(fctid, "Creating master bias from %d bias frames ...",
                 raw_bias_count);


    /* load images */

    raw_bias_list = (GiImage**)cx_calloc(raw_bias_count + 1, sizeof(GiImage*));

    raw_bias_list[raw_bias_count] = NULL;

    curr_frame = cpl_frameset_find(set, GIFRAME_BIAS);

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

        raw_bias_list[i] = giraffe_image_new(CPL_TYPE_DOUBLE);

        e_code = giraffe_image_load(raw_bias_list[i],
                                    cpl_frame_get_filename(curr_frame),
                                    GIMASTERBIAS_BIAS_EXTENSION_IMG);

        if (e_code != 0) {

            cpl_msg_error(fctid, "Could not load raw Bias Image [%s], "
                          "aborting...", cpl_frame_get_filename(curr_frame));

            for (j = 0; j <= i; ++j) {
                if (raw_bias_list[j] != NULL) {
                    giraffe_image_delete(raw_bias_list[j]);
                    raw_bias_list[j] = NULL;
                }
            }

            cx_free(raw_bias_list);

            giraffe_stacking_config_destroy(stack_config);
            stack_config = NULL;

            return -1;

        }
        else {
            curr_frame = cpl_frameset_find(set, NULL);
        }
    }

    if (mbias_config.correctbadpixels == TRUE) {

        /* load bad pixel image */

        bad_pixel_frame = cpl_frameset_find(set, GIFRAME_BADPIXEL_MAP);

        cpl_msg_info(fctid, "Bad Pixel Frame is : %s.",
                     cpl_frame_get_filename(bad_pixel_frame));

        bad_pixels = giraffe_image_new(CPL_TYPE_INT);

        e_code = giraffe_image_load(bad_pixels,
                                    cpl_frame_get_filename(bad_pixel_frame),
                                    GIMASTERBIAS_BAD_PIXEL_EXTENSION);

        if (e_code !=0 ) {
            cpl_msg_error(fctid, "Could not load Bad Pixel Image [%s], "
                          "aborting...",
                          cpl_frame_get_filename(bad_pixel_frame));

            for (j = 0; j < raw_bias_count; j++) {
                if (raw_bias_list[j] != NULL) {
                    giraffe_image_delete(raw_bias_list[j]);
                }
            }

            cx_free(raw_bias_list);

            giraffe_stacking_config_destroy(stack_config);
            stack_config = NULL;

            return -1;
        }
    }


    /*
     *  Stack the bias frames...
     */

    master_bias = giraffe_stacking_stack_images(raw_bias_list, stack_config);

    if (master_bias == NULL) {
        cpl_msg_error(fctid,"Stacking of raw bias frames failed! "
            "No master bias was created, aborting...");

        for (j = 0; j < raw_bias_count; j++) {
            if (raw_bias_list[j] != NULL) {
                giraffe_image_delete(raw_bias_list[j]);
            }
        }

        cx_free(raw_bias_list);

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

        giraffe_stacking_config_destroy(stack_config);
        stack_config = NULL;

        return -1;

    }

    properties = giraffe_image_get_properties(raw_bias_list[0]);
    e_code = giraffe_image_set_properties(master_bias, properties);

    giraffe_stacking_config_destroy(stack_config);
    stack_config = NULL;


    /*
     *  Bad pixel cleaning on result, if necessary...
     */

    if (mbias_config.correctbadpixels == TRUE) {

        cpl_msg_info(fctid, "Cleaning bad pixels on created "
                     "master bias image.");

        if (gimasterbias_remove_badpixels(master_bias, bad_pixels) != 0) {

            cpl_msg_error(fctid, "Bad pixel cleaning failed, aborting...");

            for (j = 0; j < raw_bias_count; j++) {
                if (raw_bias_list[j] != NULL) {
                    giraffe_image_delete(raw_bias_list[j]);
                }
            }

            cx_free(raw_bias_list);

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

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

            return -1;

        }
    }

    /*
     *  Remove overscans, if necessary...
     */

    if (mbias_config.removeoverscan == TRUE) {

        cpl_msg_info(fctid, "Removing overscan areas from "
                     "master bias image");

        if (giraffe_trim_raw_areas(master_bias) != 0) {

            cpl_msg_error(fctid, "Removing overscan areas from master "
                          "bias failed, aborting...");

            for (j = 0; j < raw_bias_count; j++) {
                if (raw_bias_list[j] != NULL) {
                    giraffe_image_delete(raw_bias_list[j]);
                }
            }

            cx_free(raw_bias_list);

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

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

            return -1;
        }

    }


    /*
     * Update master bias properties, save the master bias frame
     * and register it as product.
     */

    cpl_msg_info(fctid, "Writing master bias image ...");

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

    cpl_propertylist_update_double(properties, GIALIAS_CRPIX1, 1.);

    cpl_propertylist_update_double(properties, GIALIAS_EXPTTOT, 0.0);
    cpl_propertylist_update_int(properties, GIALIAS_DATANCOM, raw_bias_count);
    cpl_propertylist_update_double(properties, GIALIAS_BZERO, 0.0);

    cpl_propertylist_update_double(properties, GIALIAS_BIASVALUE,
                                   cpl_image_get_mean(giraffe_image_get(master_bias)));
    cpl_propertylist_update_double(properties, GIALIAS_BIASSIGMA,
                                   cpl_image_get_stdev(giraffe_image_get(master_bias)));

    cpl_propertylist_erase(properties, GIALIAS_EXPTIME);
    cpl_propertylist_erase(properties, GIALIAS_TPLEXPNO);


    /*
     *  Clean up bias list and bad pixels...
     */

    for (j = 0; j < raw_bias_count; j++) {
        if (raw_bias_list[j] != NULL) {
            giraffe_image_delete(raw_bias_list[j]);
        }
    }

    cx_free(raw_bias_list);
    raw_bias_list = NULL;

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


    giraffe_image_add_info(master_bias, &info, set);

    product_frame = giraffe_frame_create_image(master_bias,
                                               GIFRAME_BIAS_MASTER,
                                               CPL_FRAME_LEVEL_FINAL,
                                               TRUE, TRUE);

    if (product_frame == NULL) {

        cpl_msg_error(fctid, "Cannot create local file! Aborting ...");

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

        return -1;

    }

    cpl_frameset_insert(set, product_frame);


    /*
     *  Create bad pixel map based on masterbias
     *  Very simple algorithm, should be refined later...
     */

    if ((mbias_config.bpm.create == TRUE) && (master_bias != NULL)
        && (bad_pixel_count == 0)) {

        const cpl_image* _master_bias = giraffe_image_get(master_bias);

        const cxdouble* pd_mbias =
                cpl_image_get_data_double_const(_master_bias);


        /*cxint e_code2 = 0;*/
        /*cxint row_min = 0;*/
        /*cxint row_max = 0;*/
        cxint ncol = cpl_image_get_size_x(_master_bias);
        cxint nrow = cpl_image_get_size_y(_master_bias);
        cxint* pi_bpm = NULL;

        cxlong npix = ncol * nrow;
        cxlong nbpx = 0L;
        cxlong nbpx_max = (cxlong)(mbias_config.bpm.fraction * npix + 0.5);

        cxdouble median = 0.;
        cxdouble median_max = CX_MINDOUBLE;
        cxdouble median_min = CX_MAXDOUBLE;
        cxdouble ron = 0.;
        cxdouble tlow = 0.;
        cxdouble thigh = 0.;

        GiImage* bpixel = giraffe_image_create(CPL_TYPE_INT, ncol, nrow);


        cpl_msg_info(fctid, "Creating bad pixel map from master bias "
                     "frame ...");


        /*
         * Copy master bias properties to the bad pixel map and
         * re-assign the properties' handle.
         */

        /*e_code2 =*/ giraffe_image_set_properties(bpixel, properties);
        properties = giraffe_image_get_properties(bpixel);


        /*
         * Get bad pixel map pixel buffer
         */

        pi_bpm   = cpl_image_get_data_int(giraffe_image_get(bpixel));


        /*
         * Setup detection thresholds
         */

        if ((cpl_propertylist_has(properties, GIALIAS_PRSCX) == TRUE) ||
            (cpl_propertylist_has(properties, GIALIAS_OVSCX) == TRUE)) {

            cxint nvalues = 0;
            
            if (cpl_propertylist_has(properties, GIALIAS_PRSCX) == TRUE) {
    
                cxint xsz = cpl_propertylist_get_int(properties,
                        GIALIAS_PRSCX);
                
                cxdouble sdev = 0.;
                
    
                for (i = 0; i < nrow; ++i) {
                    
                    register cxint stride = i * ncol;
                    
                    cxdouble scx_mean = giraffe_array_mean(pd_mbias + stride,
                            xsz);
                    
                    for (j = 0; j < xsz; ++j) {
                        sdev += (pd_mbias[stride + j] - scx_mean) *
                                (pd_mbias[stride + j] - scx_mean);
                    }
                    
                }
                
                ron = sqrt(sdev / (cxdouble)(nrow * xsz - 1));
                ++nvalues;
                
            }
    
            if (cpl_propertylist_has(properties, GIALIAS_OVSCX) == TRUE) {
    
                cxint xsz = cpl_propertylist_get_int(properties,
                        GIALIAS_OVSCX);
    
                cxdouble sdev = 0.;
                
    
                for (i = 0; i < nrow; ++i) {
                    
                    register cxint stride = (i + 1) * ncol - xsz;
                    
                    cxdouble scx_mean = giraffe_array_mean(pd_mbias + stride,
                            xsz);
                    
                    for (j = 0; j < xsz; ++j) {
                        sdev += (pd_mbias[stride + j] - scx_mean) *
                                (pd_mbias[stride + j] - scx_mean);
                    }
                    
                }
                
                ron += sqrt(sdev / (cxdouble)(nrow * xsz - 1));
                ++nvalues;
                
            }

            ron /= (cxdouble)nvalues;
            
        }
        else {

            if (cpl_propertylist_has(properties, GIALIAS_RON)) {

                cxdouble conad = 1;


                ron = cpl_propertylist_get_double(properties, GIALIAS_RON);

                giraffe_error_push();

                conad = giraffe_propertylist_get_conad(properties);

                if (cpl_error_get_code() != CPL_ERROR_NONE) {

                    giraffe_image_delete(bpixel);
                    bpixel = NULL;

                    giraffe_image_delete(master_bias);
                    master_bias = NULL;

                    return -1;

                }

                giraffe_error_pop();

                ron /= conad;

            }
            else {

                ron = cpl_image_get_stdev(giraffe_image_get(master_bias));

            }
            
        }

        cpl_msg_info(fctid, "Using local median +/- %.4f [ADU] as "
                "valid pixel value range", ron);


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

            register cxint stride = i * ncol;
            register cxint* bpx_row  = pi_bpm + stride;

            register const cxdouble* bias_row = pd_mbias + stride;

            register cxdouble sdev = 0.;

            median = giraffe_array_median(bias_row, ncol);

            for (j = 0; j < ncol; ++j) {
                sdev += (bias_row[j] - median) * (bias_row[j] - median);
            }
            sdev = sqrt(sdev / (cxdouble)(ncol - 1));

            if (median < median_min) {
                median_min = median;
                /*row_min = i;*/
            }

            if (median > median_max) {
                median_max = median;
                /*row_max = i;*/
            }

            tlow  = median - mbias_config.bpm.factor * ron;
            thigh = median + mbias_config.bpm.factor * ron;

            for (j = 0; j < ncol; ++j) {

                if ((bias_row[j] < tlow) || (bias_row[j] > thigh)) {
                    bpx_row[j] = 1;
                    ++nbpx;
                }
                else {
                    bpx_row[j] = 0;
                }

            }

        }


        if (nbpx > nbpx_max) {
            cpl_msg_warning(fctid, "Number of bad pixels found (%ld) exceeds "
                    "maximum number of bad pixels expected (%ld)!", nbpx,
                    nbpx_max);
        }

        properties = giraffe_image_get_properties(bpixel);

        cpl_propertylist_update_double(properties, GIALIAS_BZERO, 0.);

        cpl_propertylist_update_double(properties,
                GIALIAS_BPM_FRACTION, mbias_config.bpm.fraction);
        cpl_propertylist_set_comment(properties, GIALIAS_BPM_FRACTION,
                "Maximum fraction of bad pixels allowed.");

        cpl_propertylist_update_int(properties,
                GIALIAS_BPM_NPIX, nbpx);
        cpl_propertylist_set_comment(properties, GIALIAS_BPM_NPIX,
                "Number of pixels flagged as bad.");

        cpl_propertylist_update_double(properties,
                GIALIAS_BPM_MEDIAN_MIN, median_min);
        cpl_propertylist_set_comment(properties, GIALIAS_BPM_MEDIAN_MIN,
                "Minimum median pixel value used for bad pixel detection.");

        cpl_propertylist_update_double(properties,
                GIALIAS_BPM_MEDIAN_MAX, median_max);
        cpl_propertylist_set_comment(properties, GIALIAS_BPM_MEDIAN_MAX,
                "Maximum median pixel value used for bad pixel detection.");

        cpl_propertylist_update_double(properties,
                GIALIAS_BPM_FACTOR, mbias_config.bpm.factor);
        cpl_propertylist_set_comment(properties, GIALIAS_BPM_FACTOR,
                "Noise multiplier defining thresholds for bad pixel "
                "detection.");

        cpl_propertylist_update_double(properties,
                GIALIAS_BPM_NOISE, ron);
        cpl_propertylist_set_comment(properties, GIALIAS_BPM_NOISE,
                "Bias noise value [ADU] used for bad pixel detection.");

        giraffe_image_add_info(bpixel, &info, set);

        product_frame = giraffe_frame_create_image(bpixel,
                                                   GIFRAME_BADPIXEL_MAP,
                                                   CPL_FRAME_LEVEL_FINAL,
                                                   TRUE, TRUE);

        if (product_frame == NULL) {

            cpl_msg_error(fctid, "Cannot create local file! Aborting ...");

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

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

            return -1;

        }

        cpl_frameset_insert(set, product_frame);


        /*
         * Clean up
         */

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

    }

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

    return 0;

}


/*
 * The quality control task starts here.
 */

static cxint
giqcmasterbias(cpl_frameset* set)
{

    const cxchar* const fctid = "giqcmasterbias";


    const cxint wstart = 50;
    const cxint wsize = 100;

    cxint i;
    cxint j;
    cxint k;
    cxint nx = 0;
    cxint ny = 0;
    cxint nvalid = 0;
    cxint status = 0;
    cxint nsigma = 5;

    const cxdouble low = 100.;
    const cxdouble high = 300.;
    const cxdouble sthreshold = 2.;

    cxdouble mean = 0.;
    cxdouble median = 0.;
    cxdouble smean = 0.;
    cxdouble sum = 0.;
    cxdouble* _mbdata = NULL;
    cxdouble* _tdata = NULL;
    cxdouble sigma[nsigma];

    cpl_propertylist* properties = NULL;
    cpl_propertylist* qclog = NULL;

    cpl_vector* _sigma = NULL;

    cpl_image* _mbias = NULL;
    cpl_image* _smbias = NULL;
    cpl_image* _test = NULL;

    cpl_frame* rframe = NULL;
    cpl_frame* pframe = NULL;

    GiPaf* qc = NULL;

    GiImage* mbias = NULL;
    GiImage* bias = NULL;

    GiWindow w;


    cpl_msg_info(fctid, "Computing QC1 parameters ...");

    qc = giraffe_qclog_open(0);

    if (qc == NULL) {
        cpl_msg_error(fctid, "Cannot create QC1 log!");
        return 1;
    }

    qclog = giraffe_paf_get_properties(qc);
    cx_assert(qclog != NULL);


    /*
     * Process master bias
     */

    pframe = giraffe_get_frame(set, GIFRAME_BIAS_MASTER,
                               CPL_FRAME_GROUP_PRODUCT);

    if (pframe == NULL) {
        cpl_msg_error(fctid, "Missing product frame (%s)",
                      GIFRAME_BIAS_MASTER);

        giraffe_paf_delete(qc);
        qc = NULL;

        return 1;
    }

    cpl_msg_info(fctid, "Processing product frame '%s' (%s)",
                 cpl_frame_get_filename(pframe), cpl_frame_get_tag(pframe));

    mbias = giraffe_image_new(CPL_TYPE_DOUBLE);
    status = giraffe_image_load(mbias, cpl_frame_get_filename(pframe), 0);

    if (status != 0) {
        cpl_msg_error(fctid, "Could not load master bias '%s'! Aborting ...",
                      cpl_frame_get_filename(pframe));

        giraffe_image_delete(mbias);
        mbias = NULL;

        giraffe_paf_delete(qc);
        qc = NULL;

        return 1;
    }


    /*
     * Load first raw image as reference
     */

    rframe = cpl_frameset_find(set, GIFRAME_BIAS);

    if (rframe == NULL) {
        cpl_msg_error(fctid, "Missing raw frame (%s)", GIFRAME_BIAS);

        giraffe_image_delete(mbias);
        mbias = NULL;

        giraffe_paf_delete(qc);
        qc = NULL;

        return 1;
    }

    bias = giraffe_image_new(CPL_TYPE_DOUBLE);
    status = giraffe_image_load(bias, cpl_frame_get_filename(rframe), 0);

    if (status != 0) {
        cpl_msg_error(fctid, "Could not load bias '%s'!",
                      cpl_frame_get_filename(rframe));

        giraffe_image_delete(bias);
        bias = NULL;

        giraffe_image_delete(mbias);
        mbias = NULL;

        giraffe_paf_delete(qc);
        qc = NULL;

        return 1;

    }

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

    giraffe_propertylist_copy(qclog, "ARCFILE", properties, GIALIAS_ARCFILE);
    giraffe_propertylist_copy(qclog, "TPL.ID", properties, GIALIAS_TPLID);

    cpl_propertylist_update_string(qclog, "PRO.CATG",
                                   cpl_frame_get_tag(pframe));
    cpl_propertylist_set_comment(qclog, "PRO.CATG",
                                 "Pipeline product category");

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

    giraffe_propertylist_copy(qclog, "PRO.DATAAVG",
                              properties, GIALIAS_DATAMEAN);
    giraffe_propertylist_copy(qclog, "PRO.DATARMS",
                              properties, GIALIAS_DATASIG);
    giraffe_propertylist_copy(qclog, "PRO.DATAMED",
                              properties, GIALIAS_DATAMEDI);
    giraffe_propertylist_copy(qclog, "PRO.DATANCOM",
                              properties, GIALIAS_DATANCOM);


    /*
     * Create screened test image for statistics computation
     */

    _mbias = giraffe_image_get(mbias);
    _mbdata = cpl_image_get_data(_mbias);

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

    nvalid = 0;

    for (i = 0; i < nx * ny; i++) {

        if (_mbdata[i] >= low && _mbdata[i] < high) {
            ++nvalid;
        }

    }

    _test = cpl_image_new(nvalid, 1, CPL_TYPE_DOUBLE);
    _tdata = cpl_image_get_data(_test);

    j = 0;

    for (i = 0; i < nx * ny; i++) {

        if (_mbdata[i] >= low && _mbdata[i] < high) {
            _tdata[j++] = _mbdata[i];
        }

    }

    cpl_propertylist_update_double(properties, GIALIAS_QCMBIASMED,
                                   cpl_image_get_median(_test));
    cpl_propertylist_set_comment(properties, GIALIAS_QCMBIASMED,
                                 "Median master bias level (ADU)");

    cpl_propertylist_update_double(properties, GIALIAS_QCMBIASAVG,
                                   cpl_image_get_mean(_test));
    cpl_propertylist_set_comment(properties, GIALIAS_QCMBIASAVG,
                                 "Mean master bias level (ADU)");

    cpl_propertylist_update_double(properties, GIALIAS_QCMBIASRMS,
                                   cpl_image_get_stdev(_test));
    cpl_propertylist_set_comment(properties, GIALIAS_QCMBIASRMS,
                                 "RMS of master bias level (ADU)");

    cpl_image_delete(_test);
    _test = NULL;

    giraffe_propertylist_copy(qclog, "QC.BIAS.MASTER.MEDIAN",
                              properties, GIALIAS_QCMBIASMED);
    giraffe_propertylist_copy(qclog, "QC.BIAS.MASTER.MEAN",
                              properties, GIALIAS_QCMBIASAVG);
    giraffe_propertylist_copy(qclog, "QC.BIAS.MASTER.RMS",
                              properties, GIALIAS_QCMBIASRMS);


    /*
     * Compute read out noise on raw frames if at least 2 raw
     * biases are available.
     */

    if (cpl_frameset_count_tags(set, GIFRAME_BIAS) > 1) {

        cpl_frame* _frame = NULL;

        cpl_image* diff = NULL;

        GiImage* _bias;


        nsigma = 5;
        memset(sigma, 0, nsigma * sizeof(cxdouble));

        _frame = cpl_frameset_find(set, GIFRAME_BIAS);
        _frame = cpl_frameset_find(set, NULL);

        _bias = giraffe_image_new(CPL_TYPE_DOUBLE);
        status = giraffe_image_load(_bias, cpl_frame_get_filename(_frame), 0);

        if (status != 0) {
            cpl_msg_error(fctid, "Could not load bias '%s'! Aborting ...",
                          cpl_frame_get_filename(_frame));

            giraffe_image_delete(_bias);
            _bias = NULL;

            giraffe_image_delete(mbias);
            mbias = NULL;

            giraffe_image_delete(bias);
            bias = NULL;

            giraffe_paf_delete(qc);
            qc = NULL;

            return 1;
        }

        diff = cpl_image_duplicate(giraffe_image_get(bias));

        if (diff == NULL) {
            cpl_msg_error(fctid, "Cannot compute bias difference image! "
                          "Aborting ...");

            giraffe_image_delete(_bias);
            _bias = NULL;

            giraffe_image_delete(mbias);
            mbias = NULL;

            giraffe_image_delete(bias);
            bias = NULL;

            giraffe_paf_delete(qc);
            qc = NULL;

            return 1;
        }

        cpl_image_subtract(diff, giraffe_image_get(_bias));

        giraffe_image_delete(_bias);


        i = 0;

        /* Lower left window */

        w.x0 = wstart;
        w.y0 = wstart;
        w.x1 = w.x0 + wsize;
        w.y1 = w.y0 + wsize;

        giraffe_error_push();

        sigma[i++] = cpl_image_get_stdev_window(diff, w.x0, w.y0, w.x1, w.y1);

        if (cpl_error_get_code() != CPL_ERROR_NONE) {
            --nsigma;
        }

        giraffe_error_pop();


        /* Lower right window */

        w.x0 = cpl_image_get_size_x(diff) - wstart - wsize;
        w.x1 = w.x0 + wsize;

        giraffe_error_push();

        sigma[i++] = cpl_image_get_stdev_window(diff, w.x0, w.y0, w.x1, w.y1);

        if (cpl_error_get_code() != CPL_ERROR_NONE) {
            --nsigma;
        }

        giraffe_error_pop();


        /* Upper right window */

        w.y0 = cpl_image_get_size_y(diff) - wstart - wsize;
        w.y1 = w.y0 + wsize;

        giraffe_error_push();

        sigma[i++] = cpl_image_get_stdev_window(diff, w.x0, w.y0, w.x1, w.y1);

        if (cpl_error_get_code() != CPL_ERROR_NONE) {
            --nsigma;
        }

        giraffe_error_pop();


        /* Upper left window */

        w.x0 = wstart;
        w.x1 = w.x0 + wsize;

        giraffe_error_push();

        sigma[i++] = cpl_image_get_stdev_window(diff, w.x0, w.y0, w.x1, w.y1);

        if (cpl_error_get_code() != CPL_ERROR_NONE) {
            --nsigma;
        }

        giraffe_error_pop();


        /* Central window */

        w.x0 = 1024;
        w.y0 = 1998;
        w.x1 = 1124;
        w.y1 = 2098;

        giraffe_error_push();

        sigma[i++] = cpl_image_get_stdev_window(diff, w.x0, w.y0, w.x1, w.y1);

        if (cpl_error_get_code() != CPL_ERROR_NONE) {
            --nsigma;
        }

        giraffe_error_pop();


        for (i = 0; i < nsigma; i++) {
            sigma[i] /= sqrt(2.);
        }

        if (nsigma < 1) {
            cpl_msg_error(fctid, "Could not compute image statistics in any "
                          "window! Aborting ...");

            cpl_image_delete(diff);
            diff = NULL;

            giraffe_image_delete(mbias);
            mbias = NULL;

            giraffe_image_delete(bias);
            bias = NULL;

            giraffe_paf_delete(qc);
            qc = NULL;

            return 1;
        }


        _sigma = cpl_vector_wrap(nsigma, sigma);

        median = cpl_vector_get_median(_sigma);

        cpl_vector_unwrap(_sigma);
        _sigma = NULL;

        cpl_image_delete(diff);
        diff = NULL;


        cpl_propertylist_update_double(properties, GIALIAS_QCRON, median);
        cpl_propertylist_set_comment(properties, GIALIAS_QCRON,
                                     "Readout noise (raw)");

        giraffe_propertylist_copy(qclog, "QC.OUT1.RON.RAW", properties,
                           GIALIAS_QCRON);
    }

    giraffe_image_delete(bias);
    bias = NULL;


    /*
     * Compute read out noise on master bias frame
     */

    k = 0;
    nsigma = 5;
    memset(sigma, 0, nsigma * sizeof(cxdouble));


    /* Lower left window */

    w.x0 = wstart;
    w.y0 = wstart;
    w.x1 = w.x0 + wsize;
    w.y1 = w.y0 + wsize;

    _smbias = cpl_image_extract(_mbias, w.x0, w.y0, w.x1, w.y1);
    _mbdata = cpl_image_get_data(_smbias);

    nvalid  = 0;

    for (i = 0; i < wsize * wsize; i++) {

        if (_mbdata[i] >= low && _mbdata[i] < high) {
            ++nvalid;
        }

    }

    _test = cpl_image_new(nvalid, 1, CPL_TYPE_DOUBLE);
    _tdata = cpl_image_get_data(_test);

    j = 0;

    for (i = 0; i < wsize * wsize; i++) {

        if (_mbdata[i] >= low && _mbdata[i] < high) {
            _tdata[j++] = _mbdata[i];
        }

    }

    giraffe_error_push();

    sigma[k++] = cpl_image_get_stdev(_test);

    if (cpl_error_get_code() != CPL_ERROR_NONE) {
        --nsigma;
    }

    giraffe_error_pop();

    cpl_image_delete(_smbias);
    _smbias = NULL;

    cpl_image_delete(_test);
    _test = NULL;

    /* Lower right window */

    w.x0 = cpl_image_get_size_x(_mbias) - wstart - wsize;
    w.x1 = w.x0 + wsize;

    _smbias = cpl_image_extract(_mbias, w.x0, w.y0, w.x1, w.y1);
    _mbdata = cpl_image_get_data(_smbias);

    nvalid  = 0;

    for (i = 0; i < wsize * wsize; i++) {

        if (_mbdata[i] >= low && _mbdata[i] < high) {
            ++nvalid;
        }

    }

    _test = cpl_image_new(nvalid, 1, CPL_TYPE_DOUBLE);
    _tdata = cpl_image_get_data(_test);

    j = 0;

    for (i = 0; i < wsize * wsize; i++) {

        if (_mbdata[i] >= low && _mbdata[i] < high) {
            _tdata[j++] = _mbdata[i];
        }

    }

    giraffe_error_push();

    sigma[k++] = cpl_image_get_stdev(_test);

    if (cpl_error_get_code() != CPL_ERROR_NONE) {
        --nsigma;
    }

    giraffe_error_pop();

    cpl_image_delete(_smbias);
    _smbias = NULL;

    cpl_image_delete(_test);
    _test = NULL;

    /* Upper right window */

    w.y0 = cpl_image_get_size_y(_mbias) - wstart - wsize;
    w.y1 = w.y0 + wsize;

    _smbias = cpl_image_extract(_mbias, w.x0, w.y0, w.x1, w.y1);
    _mbdata = cpl_image_get_data(_smbias);

    nvalid  = 0;

    for (i = 0; i < wsize * wsize; i++) {

        if (_mbdata[i] >= low && _mbdata[i] < high) {
            ++nvalid;
        }

    }

    _test = cpl_image_new(nvalid, 1, CPL_TYPE_DOUBLE);
    _tdata = cpl_image_get_data(_test);

    j = 0;

    for (i = 0; i < wsize * wsize; i++) {

        if (_mbdata[i] >= low && _mbdata[i] < high) {
            _tdata[j++] = _mbdata[i];
        }

    }

    giraffe_error_push();

    sigma[k++] = cpl_image_get_stdev(_test);

    if (cpl_error_get_code() != CPL_ERROR_NONE) {
        --nsigma;
    }

    giraffe_error_pop();

    cpl_image_delete(_smbias);
    _smbias = NULL;

    cpl_image_delete(_test);
    _test = NULL;

    /* Upper left window */

    w.x0 = wstart;
    w.x1 = w.x0 + wsize;

    _smbias = cpl_image_extract(_mbias, w.x0, w.y0, w.x1, w.y1);
    _mbdata = cpl_image_get_data(_smbias);

    nvalid  = 0;

    for (i = 0; i < wsize * wsize; i++) {

        if (_mbdata[i] >= low && _mbdata[i] < high) {
            ++nvalid;
        }

    }

    _test = cpl_image_new(nvalid, 1, CPL_TYPE_DOUBLE);
    _tdata = cpl_image_get_data(_test);

    j = 0;

    for (i = 0; i < wsize * wsize; i++) {

        if (_mbdata[i] >= low && _mbdata[i] < high) {
            _tdata[j++] = _mbdata[i];
        }

    }

    giraffe_error_push();

    sigma[k++] = cpl_image_get_stdev(_test);

    if (cpl_error_get_code() != CPL_ERROR_NONE) {
        --nsigma;
    }

    giraffe_error_pop();

    cpl_image_delete(_smbias);
    _smbias = NULL;

    cpl_image_delete(_test);
    _test = NULL;

    /* Central window */

    w.x0 = 1024;
    w.y0 = 1998;
    w.x1 = 1124;
    w.y1 = 2098;

    _smbias = cpl_image_extract(_mbias, w.x0, w.y0, w.x1, w.y1);
    _mbdata = cpl_image_get_data(_smbias);

    nvalid  = 0;

    for (i = 0; i < wsize * wsize; i++) {

        if (_mbdata[i] >= low && _mbdata[i] < high) {
            ++nvalid;
        }

    }

    _test = cpl_image_new(nvalid, 1, CPL_TYPE_DOUBLE);
    _tdata = cpl_image_get_data(_test);

    j = 0;

    for (i = 0; i < wsize * wsize; i++) {

        if (_mbdata[i] >= low && _mbdata[i] < high) {
            _tdata[j++] = _mbdata[i];
        }

    }

    giraffe_error_push();

    sigma[k++] = cpl_image_get_stdev(_test);

    if (cpl_error_get_code() != CPL_ERROR_NONE) {
        --nsigma;
    }

    giraffe_error_pop();

    cpl_image_delete(_smbias);
    _smbias = NULL;

    cpl_image_delete(_test);
    _test = NULL;

    if (nsigma < 1) {
        cpl_msg_error(fctid, "Could not compute image statistics in any "
                      "window! Aborting ...");

        giraffe_image_delete(mbias);
        mbias = NULL;

        giraffe_paf_delete(qc);
        qc = NULL;

        return 1;
    }

    _sigma = cpl_vector_wrap(nsigma, sigma);

    median = cpl_vector_get_median(_sigma);

    cpl_vector_unwrap(_sigma);
    _sigma = NULL;

    cpl_propertylist_update_double(properties, GIALIAS_QCMRON, median);
    cpl_propertylist_set_comment(properties, GIALIAS_QCMRON, "Readout noise "
                          "(master)");

    giraffe_propertylist_copy(qclog, "QC.OUT1.RON.MASTER", properties,
                       GIALIAS_QCMRON);


    /*
     * Compute structure along the x and y axes of the master bias.
     */

    _test = cpl_image_collapse_create(_mbias, 0);
    cpl_image_divide_scalar(_test, cpl_image_get_size_y(_mbias));

    mean = cpl_image_get_mean(_test);


    /*
     * Compute screened mean value
     */

    nvalid = 0;
    sum = 0.;

    _tdata = cpl_image_get_data(_test);

    for (i = 0; i < cpl_image_get_size_x(_test); i++) {

        if ((_tdata[i] > mean - sthreshold) &&
            (_tdata[i] < mean + sthreshold)) {
            sum += _tdata[i];
            ++nvalid;
        }

    }

    smean = sum / nvalid;


    /*
     * Compute RMS with respect to the screened mean value
     */

    nvalid = 0;
    sum = 0.;

    for (i = 0; i < cpl_image_get_size_x(_test); i++) {

        if ((_tdata[i] > mean - sthreshold) &&
            (_tdata[i] < mean + sthreshold)) {
            sum += pow(_tdata[i] - smean, 2.);
            ++nvalid;
        }

    }

    sum = sqrt(sum / (nvalid - 1));

    cpl_propertylist_update_double(properties, GIALIAS_QCSTRUCTX, sum);
    cpl_propertylist_set_comment(properties, GIALIAS_QCSTRUCTX,
                                 "Structure along the x axis");

    giraffe_propertylist_copy(qclog, "QC.OUT1.STRUCT.X", properties,
                              GIALIAS_QCSTRUCTX);

    cpl_image_delete(_test);
    _test = NULL;


    _test = cpl_image_collapse_create(_mbias, 1);
    cpl_image_divide_scalar(_test, cpl_image_get_size_x(_mbias));

    mean = cpl_image_get_mean(_test);


    /*
     * Compute screened mean value
     */

    nvalid = 0;
    sum = 0.;

    _tdata = cpl_image_get_data(_test);

    for (i = 0; i < cpl_image_get_size_y(_test); i++) {

        if ((_tdata[i] > mean - sthreshold) &&
            (_tdata[i] < mean + sthreshold)) {
            sum += _tdata[i];
            ++nvalid;
        }

    }

    smean = sum / nvalid;


    /*
     * Compute RMS with respect to the screened mean value
     */

    nvalid = 0;
    sum = 0.;

    _tdata = cpl_image_get_data(_test);

    for (i = 0; i < cpl_image_get_size_y(_test); i++) {

        if ((_tdata[i] > mean - sthreshold) &&
            (_tdata[i] < mean + sthreshold)) {
            sum += pow(_tdata[i] - smean, 2.);
            ++nvalid;
        }

    }

    sum = sqrt(sum / (nvalid - 1));

    cpl_propertylist_update_double(properties, GIALIAS_QCSTRUCTY, sum);
    cpl_propertylist_set_comment(properties, GIALIAS_QCSTRUCTY,
                                 "Structure along the y axis");

    giraffe_propertylist_copy(qclog, "QC.OUT1.STRUCT.Y", properties,
                       GIALIAS_QCSTRUCTY);

    cpl_image_delete(_test);
    _test = NULL;


    /*
     * Write QC1 log and save updated master bias.
     */

    giraffe_image_save(mbias, cpl_frame_get_filename(pframe));

    giraffe_image_delete(mbias);
    mbias = NULL;

    giraffe_qclog_close(qc);
    qc = NULL;

    return 0;

}


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

int
cpl_plugin_get_info(cpl_pluginlist* list)
{

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


    cpl_plugin_init(plugin,
                    CPL_PLUGIN_API,
                    GIRAFFE_BINARY_VERSION,
                    CPL_PLUGIN_TYPE_RECIPE,
                    "gimasterbias",
                    "Creates a master bias image from a set of raw biases.",
                    "For detailed information please refer to the "
                    "GIRAFFE pipeline user manual.\nIt is available at "
                    "http://www.eso.org/pipelines.",
                    "Giraffe Pipeline",
                    PACKAGE_BUGREPORT,
                    giraffe_get_license(),
                    gimasterbias_create,
                    gimasterbias_exec,
                    gimasterbias_destroy);

    cpl_pluginlist_append(list, plugin);

    return 0;

}
