/*
 * 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 <cxlist.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 "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"


static cxint gimasterdark(cpl_parameterlist*, cpl_frameset*);
static cxint giqcmasterdark(cpl_frameset*);


static cxint
_giraffe_clean_badpixels(GiImage* image, GiImage* bpixel)
{

    cxint i = 0;

    cxint nx_image = 0;
    cxint ny_image = 0;
    cxint ny_mask = 0;
    cxint shift = 0;
    cxint* _bpixel = NULL;

    cxdouble* _image = NULL;

    cpl_propertylist* properties = NULL;


    cx_assert(image != NULL);
    cx_assert(bpixel != NULL);

    nx_image = cpl_image_get_size_y(giraffe_image_get(image));
    ny_image = cpl_image_get_size_x(giraffe_image_get(image));

    ny_mask = cpl_image_get_size_x(giraffe_image_get(bpixel));

    properties = giraffe_image_get_properties(bpixel);
    shift = cpl_propertylist_get_int(properties, GIALIAS_PRSCX);

    _image = cpl_image_get_data_double(giraffe_image_get(image));
    _bpixel = cpl_image_get_data_int(giraffe_image_get(bpixel));

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

        cxint j = 0;

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

            if (_bpixel[i * ny_mask + j + shift] != 0) {
                _image[i * ny_image + j] = 0.;
            }

        }

    }

    return 0;

}


/*
 * 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
gimasterdark_create(cpl_plugin* plugin)
{

    cpl_parameter* p = NULL;

    cpl_recipe* recipe = (cpl_recipe*)plugin;


    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.
     */

    /* Frame combination */

    giraffe_stacking_config_add(recipe->parameters);
    p = cpl_parameterlist_find(recipe->parameters, "giraffe.stacking.method");

    if ( p != NULL) {
        cpl_parameter_set_default_string(p, "median");
    }

    /* Bias removal */

    giraffe_bias_config_add(recipe->parameters);

    return 0;

}

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

static cxint
gimasterdark_exec(cpl_plugin* plugin)
{

    cpl_recipe* recipe = (cpl_recipe*)plugin;

    cxint status = 0;


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

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

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

    status = giqcmasterdark(recipe->frames);

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

    return 0;

}


static cxint
gimasterdark_destroy(cpl_plugin* plugin)
{

    cpl_recipe* recipe = (cpl_recipe*)plugin;


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

    cpl_parameterlist_delete(recipe->parameters);

    giraffe_error_clear();

    return 0;

}


/*
 * The actual recipe starts here.
 */

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

    const cxchar* const _id = "gimasterdark";

    cxint i = 0;
    cxint status = 0;
    cxint count = 0;

    cxdouble exptotal = 0.;

    cx_list* darks = NULL;

    cx_list_const_iterator position = NULL;

    cpl_matrix* bias_areas = NULL;

    cpl_frame* dark_frame = NULL;
    cpl_frame* mbias_frame = NULL;
    cpl_frame* bpixel_frame = NULL;
    cpl_frame* mdark_frame = NULL;

    cpl_image* _dark = NULL;

    cpl_propertylist* properties = NULL;


    GiImage* result = NULL;
    GiImage* mbias = NULL;
    GiImage* bpixel = NULL;
    GiImage** stack = NULL;

    GiBiasConfig* bias_config = NULL;

    GiStackingConfig* stack_config = NULL;

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

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



    /*
     * Set frame group information
     */

    status = giraffe_frameset_set_groups(set, groups);

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


    /*
     * Count the number of available raw frames
     */

    count = cpl_frameset_count_tags(set, GIFRAME_DARK);

    if (count == 0) {
        cpl_msg_error(_id, "No raw dark frames found in frameset! "
                "Aborting ...");
        return 1;
    }


    /*
     * Verify frameset contents
     */

    mbias_frame = cpl_frameset_find(set, GIFRAME_BIAS_MASTER);

    if (!mbias_frame) {
        cpl_msg_error(_id, "No master bias present in frame set. "
                      "Aborting ...");
        return 1;
    }

    bpixel_frame = cpl_frameset_find(set, GIFRAME_BADPIXEL_MAP);

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


    /*
     * Load the raw data frames
     */

    cpl_msg_info(_id, "Loading dark frames ...");

    darks = cx_list_new();
    dark_frame = cpl_frameset_find(set, GIFRAME_DARK);

    i = 0;

    while ((dark_frame != NULL ) && (i < count)) {

        const cxchar* const filename = cpl_frame_get_filename(dark_frame);

        GiImage* dark = giraffe_image_new(CPL_TYPE_DOUBLE);


        status = giraffe_image_load(dark, filename, 0);

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

            cx_list_destroy(darks, (cx_free_func)giraffe_image_delete);
            darks = NULL;

            return 1;
        }

        cx_list_push_back(darks, dark);

        dark_frame = cpl_frameset_find(set, NULL);
        ++i;

    }

    cx_assert(i == count);


    /*
     * Prepare for bias subtraction
     */

    bias_config = giraffe_bias_config_create(config);

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

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

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

            giraffe_bias_config_destroy(bias_config);
            bias_config = NULL;

            cx_list_destroy(darks, (cx_free_func)giraffe_image_delete);
            darks = NULL;

            return 1;
        }
        else {

            const cxchar* filename = cpl_frame_get_filename(mbias_frame);


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

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

                giraffe_image_delete(mbias);
                mbias = NULL;

                giraffe_bias_config_destroy(bias_config);
                bias_config = NULL;

                cx_list_destroy(darks, (cx_free_func)giraffe_image_delete);
                darks = NULL;

                return 1;
            }

        }
    }


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

     if (bpixel_frame) {

        const cxchar* filename = cpl_frame_get_filename(bpixel_frame);


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

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

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

            giraffe_bias_config_destroy(bias_config);
            bias_config = NULL;

            cx_list_destroy(darks, (cx_free_func)giraffe_image_delete);
            darks = NULL;

            return 1;
        }

    }


    /*
     * Subtract the bias from each dark.
     */

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

        GiImage* dark = cx_list_pop_front(darks);
        GiImage* rdark = giraffe_image_new(CPL_TYPE_DOUBLE);


        status = giraffe_bias_remove(rdark, dark, mbias, bpixel, bias_areas,
                                     bias_config);

        if (status != 0) {

            cx_list_push_front(darks, dark);

            giraffe_image_delete(rdark);
            rdark = NULL;

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

            giraffe_bias_config_destroy(bias_config);
            bias_config = NULL;

            cx_list_destroy(darks, (cx_free_func)giraffe_image_delete);
            darks = NULL;

            return 1;

        }

        giraffe_image_delete(dark);
        dark = NULL;

        cx_list_push_back(darks, rdark);

    }

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

    giraffe_bias_config_destroy(bias_config);
    bias_config = NULL;


    /*
     * Scale each bias subtracted dark to 1 second exposure time.
     * Compute total exposure time.
     */

    position = cx_list_begin(darks);

    while (position != cx_list_end(darks)) {

        cxdouble exptime = 0.;

        GiImage* dark = cx_list_get(darks, position);

        properties = giraffe_image_get_properties(dark);
        exptime = cpl_propertylist_get_double(properties, GIALIAS_EXPTIME);

        cpl_image_divide_scalar(giraffe_image_get(dark), exptime);
        exptotal += exptime;

        cpl_propertylist_update_double(properties, GIALIAS_EXPTIME, 1.);

        position = cx_list_next(darks, position);

    }


    /*
     * Check that enough raw frames are present in the frameset
     * for the selected frame combination method.
     */

    stack_config = giraffe_stacking_config_create(config);

    count = cx_list_size(darks);

    if (count < stack_config->min_nr_frames) {

        cpl_msg_error(_id, "Not enough frames (%d). Stacking method '%d' "
                      "requires at least %d frames! Aborting...", count,
                      stack_config->stackmethod, stack_config->min_nr_frames);

        giraffe_stacking_config_destroy(stack_config);
        stack_config = NULL;

        return 1;

    }


    /*
     * Combine the raw dark frames
     */

    cpl_msg_info(_id, "Combining %d of %d dark frames.", i, count);

    stack = cx_calloc(count + 1, sizeof(GiImage*));

    i = 0;
    position = cx_list_begin(darks);

    while (position != cx_list_end(darks)) {
        stack[i] = cx_list_get(darks, position);
        position = cx_list_next(darks, position);
        ++i;
    }

    result = giraffe_stacking_stack_images(stack, stack_config);

    if (result == NULL) {

        cpl_msg_error(_id, "Frame combination failed! Aborting ...");

        cx_free(stack);
        stack = NULL;

        cx_list_destroy(darks, (cx_free_func)giraffe_image_delete);
        darks = NULL;

        giraffe_stacking_config_destroy(stack_config);
        stack_config = NULL;

        return 1;

    }

    properties = giraffe_image_get_properties(stack[0]);
    giraffe_image_set_properties(result, properties);

    cx_free(stack);
    stack = NULL;

    giraffe_stacking_config_destroy(stack_config);
    stack_config = NULL;

    cx_list_destroy(darks, (cx_free_func)giraffe_image_delete);
    darks = NULL;


    /*
     * Clean the bad pixels if a bad pixel map is present. The flux of
     * bad pixels is simply set to 0.
     */

    if (bpixel) {

        status = _giraffe_clean_badpixels(result, bpixel);

        if (status != 0) {

            cpl_msg_error(_id, "Bad pixel cleaning on master dark frame "
                          "failed!");

            giraffe_image_delete(result);
            result = NULL;

            giraffe_image_delete(bpixel);
            bpixel = NULL;

            return 1;

        }

    }

    giraffe_image_delete(bpixel);
    bpixel = NULL;


    /*
     * Update master dark properties.
     */

    cpl_msg_info(_id, "Writing master dark image ...");

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

    cpl_propertylist_update_double(properties, GIALIAS_CRPIX1, 1.);

    cpl_propertylist_update_double(properties, GIALIAS_EXPTTOT, exptotal);
    cpl_propertylist_update_int(properties, GIALIAS_DATANCOM, count);

    _dark = giraffe_image_get(result);

    cpl_propertylist_update_double(properties, GIALIAS_DARKVALUE,
                                   cpl_image_get_mean(_dark));
    cpl_propertylist_set_comment(properties, GIALIAS_DARKVALUE,
                                 "Dark current in ADU/s");

    cpl_propertylist_erase(properties, GIALIAS_TPLEXPNO);


    giraffe_image_add_info(result, &info, set);

    mdark_frame = giraffe_frame_create_image(result,
                                             GIFRAME_DARK_MASTER,
                                             CPL_FRAME_LEVEL_FINAL,
                                             TRUE, TRUE);

    if (mdark_frame == NULL) {

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

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

        return 1;

    }

    cpl_frameset_insert(set, mdark_frame);


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

    return 0;

}


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

static cxint
giqcmasterdark(cpl_frameset* set)
{

    const cxchar* const fctid = "giqcmasterdark";

    cxint status = 0;

    cxdouble mean = 0.;
    cxdouble exptime = 1.;
    cxdouble gflux = 0.;

    cpl_size gpx = 0;
    cpl_size gpy = 0;

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


    cpl_image* _mdark = NULL;

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

    GiPaf* qc = NULL;

    GiImage* mdark = NULL;
    GiImage* dark = NULL;

    GiWindow w = {10, 200, 2038, 3000};


    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 dark
     */

    pframe = giraffe_get_frame(set, GIFRAME_DARK_MASTER,
                               CPL_FRAME_GROUP_PRODUCT);

    if (pframe == NULL) {
        cpl_msg_error(fctid, "Missing product frame (%s)",
                      GIFRAME_DARK_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));

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

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

        giraffe_image_delete(mdark);
        mdark = NULL;

        giraffe_paf_delete(qc);
        qc = NULL;

        return 1;
    }


    /*
     * Load first raw image as reference
     */

    rframe = cpl_frameset_find(set, GIFRAME_DARK);

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

        giraffe_image_delete(mdark);
        mdark = NULL;

        giraffe_paf_delete(qc);
        qc = NULL;

        return 1;
    }

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

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

        giraffe_image_delete(dark);
        dark = NULL;

        giraffe_image_delete(mdark);
        mdark = NULL;

        giraffe_paf_delete(qc);
        qc = NULL;

        return 1;

    }

    properties = giraffe_image_get_properties(dark);
    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(mdark);
    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);


    /*
     * Get 1 over exposure time in hours from the master dark frame
     */

    if (cpl_propertylist_has(properties, GIALIAS_EXPTIME) == TRUE) {

        exptime = cpl_propertylist_get_double(properties, GIALIAS_EXPTIME);

    }

    exptime = 3600. / exptime;


    /*
     * Compute average dark value on a central window of the
     * master dark frame. The window is used to exclude the
     * glow feature in the upper part of the CCD.
     */

    _mdark = giraffe_image_get(mdark);

    mean = cpl_image_get_mean_window(_mdark, w.x0, w.y0, w.x1, w.y1);


    cpl_propertylist_update_double(properties, GIALIAS_QCMDARKAVG,
                                   mean * exptime);
    cpl_propertylist_set_comment(properties, GIALIAS_QCMDARKAVG,
                                 "Mean master dark current (ADU/hr)");

    giraffe_propertylist_copy(qclog, "QC.DARK.CURRENT", properties,
                              GIALIAS_QCMDARKAVG);

    /*
     * Monitoring the glow in the upper right part of the CCD
     */

    w.x0 = 1350;
    w.x1 = 2048;
    w.y0 = 3800;
    w.y1 = 4095;

    gflux = cpl_image_get_flux_window(_mdark, w.x0, w.y0, w.x1, w.y1);

    cpl_image_get_maxpos_window(_mdark, w.x0, w.y0, w.x1, w.y1, &gpx, &gpy);


    cpl_propertylist_update_double(properties, GIALIAS_QCGLOWFLX, gflux);
    cpl_propertylist_set_comment(properties, GIALIAS_QCGLOWFLX,
                                 "Total flux of glow feature (ADU/s)");

    cpl_propertylist_update_int(properties, GIALIAS_QCGLOWX, (cxint)gpx);
    cpl_propertylist_set_comment(properties, GIALIAS_QCGLOWX,
                                 "X position of glow feature (pxl)");

    cpl_propertylist_update_int(properties, GIALIAS_QCGLOWY, (cxint)gpy);
    cpl_propertylist_set_comment(properties, GIALIAS_QCGLOWY,
                                 "X position of glow feature (pxl)");

    giraffe_propertylist_copy(qclog, "QC.GLOW.LEVEL", properties,
                              GIALIAS_QCGLOWFLX);
    giraffe_propertylist_copy(qclog, "QC.GLOW.POSX", properties,
                              GIALIAS_QCGLOWX);
    giraffe_propertylist_copy(qclog, "QC.GLOW.POSY", properties,
                              GIALIAS_QCGLOWY);

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

    giraffe_image_save(mdark, cpl_frame_get_filename(pframe));

    giraffe_image_delete(mdark);
    mdark = 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,
                    "gimasterdark",
                    "Creates a master dark image from a set of raw dark "
                    "frames.",
                    "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(),
                    gimasterdark_create,
                    gimasterdark_exec,
                    gimasterdark_destroy);

    cpl_pluginlist_append(list, plugin);

    return 0;

}
