/*
 * 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 <cxmemory.h>
#include <cxmessages.h>
#include <cxstrutils.h>
#include <cxstring.h>

#include <cpl_msg.h>
#include <cpl_parameterlist.h>
#include <cpl_vector.h>

#include "gifiberutils.h"
#include "gifibers.h"
#include "gistacking.h"
#include "giimage.h"
#include "gimath.h"


/**
 * @defgroup gistacking Stacking of Giraffe Images
 *
 * The module implements a way of stacking Giraffe Images stored in a list in
 * different ways. Given a list of images they can be combined using
 * averaging, median, Minimum-Maximum rejection and Kappa-Sigma Clipping.
 *
 * @code
 *     #include <gistacking.h>
 * @endcode
 */

/**@{*/

/*
 * Minimum number of images required for the different stacking methods.
 */

enum {
    MIN_IMAGES_AVERAGE = 2,
    MIN_IMAGES_MEDIAN = 3,
    MIN_IMAGES_MINMAX = 3,
    MIN_IMAGES_KSIGMA = 2
};


inline static cxint
_image_array_count(GiImage **img_array)
{
    cxint count = 0;

    if (img_array == NULL) {
        return 0;
    }

    while (img_array[count] != NULL) {
        count++;
    }

    return count;

}

inline static cxbool
_image_array_same_size(GiImage **img_array)
{

    cxint num_images = 0,
          nrow,
          ncol,
          i;

    num_images = _image_array_count(img_array);

    if (num_images <= 0) {
        return FALSE;
    }

    ncol = cpl_image_get_size_x(giraffe_image_get(img_array[0]));
    nrow = cpl_image_get_size_y(giraffe_image_get(img_array[0]));

    for (i=1; i<num_images; i++) {
        if ((cpl_image_get_size_x(giraffe_image_get(img_array[i]))!=ncol) ||
            (cpl_image_get_size_y(giraffe_image_get(img_array[i]))!=nrow)) {
            return FALSE;
        }
    }

    return TRUE;

}

/**
 * @brief
 *   Stack a list of images using averaging and return the resulting image.
 *
 * @param img_array Array of input images, last arrayentry should be NULL
 * @param config Configuration data for stacking method @see GiStackingMethod
 *
 * @return Pointer to the newly created image.
 *
 * Function creates an output image which is the average of all of the input
 * images on a pixel by pixel basis.
 *
 */

GiImage*
giraffe_stacking_average(GiImage **img_array, const GiStackingConfig *config)
{

    const cxchar *fctid = "giraffe_stacking_average";

    GiImage  *result     = NULL;

    cxint     num_images = 0,
              ncol       = 0,
              nrow       = 0,
              i          = 0;
    /*cxint     e_code     = 0;*/

    cxlong    npix       = 0L;

    cxdouble *pdresult   = NULL,
              inverse    = 0.0;


    /* Not used here! */
    (void) config;

    num_images = _image_array_count(img_array);

    if (num_images<=0) {
        cpl_msg_error(fctid, "Empty array of images, aborting..." );
        return NULL;
    }

    if (_image_array_same_size(img_array)==FALSE) {
        cpl_msg_error(fctid, "Input Images are not the same size, "
                             "aborting..." );
        return NULL;
    }

    ncol = cpl_image_get_size_x(giraffe_image_get(img_array[0]));
    nrow = cpl_image_get_size_y(giraffe_image_get(img_array[0]));
    npix = ncol * nrow;

    result = giraffe_image_create(CPL_TYPE_DOUBLE, ncol, nrow);
    pdresult = cpl_image_get_data_double(giraffe_image_get(result));

    for (i = 0; i < npix; i++) {
        pdresult[i] = 0.0;
    }

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

        /*e_code =*/ cpl_image_add(giraffe_image_get(result),
                               giraffe_image_get(img_array[i]));

    }

    inverse = 1.0 / num_images;

    /*e_code =*/ cpl_image_multiply_scalar(giraffe_image_get(result), inverse);

    return result;

}

/**
 * @brief
 *   Stack a list of images using median and return the resulting image.
 *
 * @param img_array Array of input images, last arrayentry should be NULL
 * @param config Configuration data for stacking method @see GiStackingMethod
 *
 * @return Pointer to the newly created image.
 *
 * Function creates an output image which is the median value of all of the input
 * images on a pixel by pixel basis.
 *
 */

GiImage*
giraffe_stacking_median(GiImage **img_array, const GiStackingConfig *config)
{

    const cxchar *fctid = "giraffe_stacking_median";

    cxint num_images = 0;
    cxint ncol = 0;
    cxint nrow = 0;
    cxint i = 0;

    cxlong npix = 0L;
    cxlong j = 0L;

    cxdouble *pdresult = NULL;
    cxdouble **pdimgs = NULL;

    cpl_vector *zvalue = NULL;

    GiImage  *result = NULL;


    /* Not used here! */
    (void) config;

    num_images = _image_array_count(img_array);

    if (num_images <= 0) {
        cpl_msg_error(fctid, "Empty array of images, aborting..." );
        return NULL;
    }

    if (num_images < MIN_IMAGES_MEDIAN) {
        cpl_msg_error(fctid, "Not enough Images in array to perform median "
                             "stacking, aborting...");
        return NULL;
    }

    if (_image_array_same_size(img_array) == FALSE) {
        cpl_msg_error(fctid, "Input Images are not the same size, "
                             "aborting...");
        return NULL;
    }

    ncol = cpl_image_get_size_x(giraffe_image_get(img_array[0]));
    nrow = cpl_image_get_size_y(giraffe_image_get(img_array[0]));
    npix = ncol * nrow;

    result = giraffe_image_create(CPL_TYPE_DOUBLE, ncol, nrow);
    pdresult = cpl_image_get_data_double(giraffe_image_get(result));

    pdimgs = cx_calloc(num_images, sizeof(cxdouble*));

    zvalue = cpl_vector_new(num_images);

    for (i = 0; i < num_images; i++) {
        pdimgs[i] = cpl_image_get_data_double(giraffe_image_get(img_array[i]));
    }

    for (j = 0; j < npix; j++) {
        for (i = 0; i < num_images; i++) {
            cpl_vector_set(zvalue, i, (pdimgs[i])[j]);
        }
        pdresult[j] = cpl_vector_get_median(zvalue);
    }

    cpl_vector_delete(zvalue);
    zvalue = NULL;

    cx_free(pdimgs);
    pdimgs = NULL;

    return result;

}

/**
 * @brief
 *   Stack a list of images using minmax rejection
 *   and return the resulting image.
 *
 * @param img_array Array of input images, last arrayentry should be NULL
 * @param config Configuration data for stacking method @see GiStackingMethod
 *
 * @return Pointer to the newly created image.
 *
 * Function creates an output image which is the minmax rejected value of all
 * of the input images on a pixel by pixel basis.
 *
 * MinMax rejection is based on sorting the values for a specific pixel
 * coordinate x,y and discarding the @em config->rejectmin lowest values and
 * the @em config->rejectmax highest values.
 * The resulting pixel value is the average value of the remaining pixel
 * values after the discard has taken place.
 *
 * Constraints:
 * @em config->rejectmin + @em config->rejectmax < total number of images,
 * @em config->rejectmin > 0 and @em config->rejectmax > 0
 *
 */

GiImage*
giraffe_stacking_minmax(GiImage **img_array, const GiStackingConfig *config)
{

    const cxchar *fctid = "giraffe_stacking_minmax";

    cxint i = 0;
    cxint n = 0;
    cxint ncol = 0;
    cxint nrow = 0;
    cxint minn = 0;
    cxint maxn = 0;
    cxint num_images = 0;

    cxlong j = 0L;
    cxlong npix = 0L;

    cxdouble daverage = 0.;
    cxdouble dinvdeltan = 0.;
    cxdouble *pdresult = NULL;
    cxdouble **pdimgs = NULL;

    cpl_vector *zvalue = NULL;

    GiImage *result = NULL;


    num_images = _image_array_count(img_array);

    if (num_images <= 0) {
        cpl_msg_error(fctid, "Empty array of images, aborting...");
        return NULL;
    }

    if (num_images < MIN_IMAGES_MINMAX) {
        cpl_msg_error(fctid, "Not enough Images in array to perform minmax "
                             "stacking, aborting...");
        return NULL;
    }

    if (_image_array_same_size(img_array) == FALSE) {
        cpl_msg_error(fctid, "Input Images are not the same size, "
                             "aborting...");
        return NULL;
    }

    if ((config->rejectmin + config->rejectmax) >= num_images) {
        cpl_msg_error(fctid, "Max %d Input Images can be rejected, "
                             "aborting...", num_images - 1);
        return NULL;
    }

    if ((config->rejectmin == 0) || (config->rejectmax == 0)) {
        cpl_msg_error(fctid, "At least one value should be rejected [%d,%d],"
                             " aborting...", config->rejectmin,
                             config->rejectmax);
        return NULL;
    }

    ncol = cpl_image_get_size_x(giraffe_image_get(img_array[0]));
    nrow = cpl_image_get_size_y(giraffe_image_get(img_array[0]));
    npix = ncol * nrow;

    result = giraffe_image_create(CPL_TYPE_DOUBLE, ncol, nrow);
    pdresult = cpl_image_get_data_double(giraffe_image_get(result));

    minn = config->rejectmin;
    maxn = num_images - config->rejectmax;

    dinvdeltan = 1. / (maxn - minn);

    pdimgs = (cxdouble**) cx_calloc(num_images, sizeof(cxdouble*));

    zvalue = cpl_vector_new(num_images);

    for (i = 0; i < num_images; i++) {
        pdimgs[i] = cpl_image_get_data_double(giraffe_image_get(img_array[i]));
    }

    for (j = 0; j < npix; j++) {
        for (i = 0; i < num_images; i++) {
            cpl_vector_set(zvalue, i, (pdimgs[i])[j]);
        }

        cpl_vector_sort(zvalue, 1);

        daverage = 0.;

        for (n = minn; n < maxn; n++) {
            daverage += cpl_vector_get(zvalue, n);
        }

        pdresult[j] = daverage * dinvdeltan;
    }

    cpl_vector_delete(zvalue);
    zvalue = NULL;

    cx_free(pdimgs);
    pdimgs = NULL;

    return result;

}

/**
 * @brief
 *   Stack a list of images using Kappa Sigma Clipping
 *   and return the resulting image.
 *
 * @param img_array Array of input images, last arrayentry should be NULL
 * @param config Configuration data for stacking method @see GiStackingMethod
 *
 * @return Pointer to the newly created image.
 *
 * Function creates an output image which is the Kappa Sigma Clipped value of
 * all of the input images on a pixel by pixel basis.
 *
 * Kappa Sigma Clipping is based on finding first the median for
 * a specific pixel coordinate x,y and secondly determining the sigma value
 * around this median. All values less than
 * median - ( @em config->ksigmalow * sigma ) are discarded as well as all
 * values larger than median + ( @em config->ksigmahigh * sigma ).
 * The resulting pixel value is the average value of the remaining pixel
 * values after the discard has taken place.
 */

GiImage*
giraffe_stacking_ksigma(GiImage **img_array, const GiStackingConfig *config)
{

    const cxchar *fctid = "giraffe_stacking_ksigma";

    cxint i = 0;
    cxint n = 0;
    cxint ncol = 0;
    cxint nrow = 0;
    cxint num_images = 0;

    cxlong j = 0L;
    cxlong npix = 0L;
    cxlong goodpix = 0L;

    cxdouble *pdresult = NULL;
    cxdouble **pdimgs = NULL;

    cpl_vector *zvalue = NULL;

    GiImage *result = NULL;


    num_images = _image_array_count(img_array);

    if (num_images <= 0) {
        cpl_msg_error(fctid, "Empty array of images, aborting...");
        return NULL;
    }

    if (num_images < MIN_IMAGES_KSIGMA) {
        cpl_msg_error(fctid, "Not enough Images in array to perform "
                             "kappa-sigma stacking, aborting...");
        return NULL;
    }

    if (_image_array_same_size(img_array) == FALSE) {
        cpl_msg_error(fctid, "Input Images are not the same size, "
                             "aborting...");
        return NULL;
    }

    ncol = cpl_image_get_size_x(giraffe_image_get(img_array[0]));
    nrow = cpl_image_get_size_y(giraffe_image_get(img_array[0]));
    npix = ncol * nrow;

    result = giraffe_image_create(CPL_TYPE_DOUBLE, ncol, nrow);
    pdresult = cpl_image_get_data_double(giraffe_image_get(result));

    pdimgs = (cxdouble**) cx_calloc(num_images, sizeof(cxdouble*));

    zvalue = cpl_vector_new(num_images);

    for (i = 0; i < num_images; i++) {
        pdimgs[i] = cpl_image_get_data_double(giraffe_image_get(img_array[i]));
    }

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

        cxdouble median = 0.;
        cxdouble sigma = 0.;
        cxdouble sum = 0.;
        cxdouble low_median = 0.;
        cxdouble high_median = 0.;


        for (i = 0; i < num_images; i++) {
            cpl_vector_set(zvalue, i, (pdimgs[i])[j]);
        }

        median = cpl_vector_get_median(zvalue);

        for (n = 0; n < num_images; n++) {
            sigma += fabs(cpl_vector_get(zvalue, n) - median);
        }

        sigma /= num_images;

        low_median  = median - ( sigma * config->ksigmalow );
        high_median = median + ( sigma * config->ksigmahigh );

        sum = 0.;

        goodpix = num_images;

        for (n = 0; n < num_images; n++) {

            cxdouble _zvalue = cpl_vector_get(zvalue, n);

            if ((_zvalue  < low_median) || (_zvalue > high_median)) {
                --goodpix;
            }
            else {
                sum += _zvalue;
            }

        }

        pdresult[j] = sum / goodpix;
    }

    cpl_vector_delete(zvalue);
    zvalue = NULL;

    cx_free(pdimgs);
    pdimgs = NULL;

    return result;

}

/**
 * @brief
 *   Stack a list of images using one of four different kinds of stacking
 *   and return the resulting image.
 *
 * @param img_array Array of input images, last arrayentry should be NULL
 * @param config Configuration data for stacking method @see GiStackingMethod
 *
 * @return Pointer to the newly created image.
 *
 * Function creates an output image which is the result of stacking i.e.
 * combining the list of input images.
 *
 * Possible stacking methods are:
 * - Averaging
 * - Median
 * - Min Max Rejection
 * - Kappa-Sigma Clipping
 *
 * @see giraffe_stacking_average
 * @see giraffe_stacking_median
 * @see giraffe_stacking_minmax
 * @see giraffe_stacking_ksigma
 *
 */

GiImage*
giraffe_stacking_stack_images(GiImage **img_array,
                              const GiStackingConfig *config)
{

    const cxchar *fctid = "giraffe_stacking_stack_images";

    GiImage *giimage_out;
    cxint    num_images = 0;

    cpl_msg_debug(fctid, "Procedure Start" );

    if (config == NULL) {
        return NULL;
    }

    if (img_array == NULL) {
        return NULL;
    }

    num_images = _image_array_count(img_array);

    switch (config->stackmethod) {

    case GISTACKING_METHOD_AVERAGE :

        cpl_msg_info(fctid, "Combination method is Average");
        cpl_msg_info(fctid, "Averaging %d images\n", num_images);

        giimage_out = giraffe_stacking_average(img_array, config);

        break;

    case GISTACKING_METHOD_MEDIAN :

        cpl_msg_info(fctid, "Combination method is Median");
        cpl_msg_info(fctid, "Finding median of %d images", num_images);

        giimage_out = giraffe_stacking_median(img_array, config);

        break;

    case GISTACKING_METHOD_MINMAX :

        cpl_msg_info(fctid, "Combination method is MinMax Rejection");
        cpl_msg_info(
            fctid,
            "Rejecting lower %d and upper %d pixel values out of possible %d",
            (cxint) (floor(num_images * config->rejectmin / 100.0)) + 1,
            (cxint) (floor(num_images * config->rejectmax / 100.0)) + 1,
            num_images
        );

        giimage_out = giraffe_stacking_minmax(img_array, config);

        break;

    case GISTACKING_METHOD_KSIGMA :

        cpl_msg_info(fctid, "Combination method is K-Sigma Clipping");
        cpl_msg_info(
            fctid,
            "K Low = %3.1f sigma, K High = %3.1f sigma",
            config->ksigmalow,
            config->ksigmahigh
        );

        giimage_out = giraffe_stacking_ksigma(img_array, config);

        break;

    case GISTACKING_METHOD_UNDEFINED :
    default :


        cpl_msg_error(fctid, "Invalid stacking method, aborting...");

        giimage_out = NULL;
        break;
    }

    cpl_msg_debug(fctid, "Procedure End" );

    return giimage_out;

}

/**
 * @brief
 *   Creates a setup structure for the stacking of images.
 *
 * @param list  Parameter list.
 *
 * @return A newly allocated and initialized setup structure if no errors
 *   occurred, or @c NULL otherwise.
 *
 * The function creates and initializes a setup structure with the values
 * taken from the parameter list @em list. If an invalid stacking method is
 * found in the parameter list the function fails returning @c NULL. In
 * addition the error code @em CPL_ERROR_ILLEGAL_INPUT is set.
 */

GiStackingConfig *
giraffe_stacking_config_create(cpl_parameterlist *list)
{

    const cxchar *fctid = "giraffe_stacking_config_create";


    cxchar *method = NULL;

    cpl_parameter *p = NULL;

    GiStackingConfig *config = NULL;


    if (list == NULL) {
        return NULL;
    }

    config = cx_calloc(1, sizeof *config);


    /*
     * Some defaults
     */

    config->stackmethod   = GISTACKING_METHOD_UNDEFINED;
    config->min_nr_frames = 0;


    /*
     *  Retrieve parameter values...
     */

    p = cpl_parameterlist_find(list, "giraffe.stacking.method");
    method = cx_strdup(cpl_parameter_get_string(p));

    p = cpl_parameterlist_find(list, "giraffe.stacking.ksigma.low");
    config->ksigmalow = cpl_parameter_get_double(p);

    p = cpl_parameterlist_find(list, "giraffe.stacking.ksigma.high");
    config->ksigmahigh = cpl_parameter_get_double(p);

    p = cpl_parameterlist_find(list, "giraffe.stacking.minmax.minimum");
    config->rejectmin = cpl_parameter_get_int(p);

    p = cpl_parameterlist_find(list, "giraffe.stacking.minmax.maximum");
    config->rejectmax = cpl_parameter_get_int(p);


    /*
     * Select method.
     */

    if (strcmp(method, "average") == 0) {
        config->stackmethod = GISTACKING_METHOD_AVERAGE;
    }

    if (strcmp(method, "median") == 0) {
        config->stackmethod = GISTACKING_METHOD_MEDIAN;
    }

    if (strcmp(method, "minmax") == 0) {
        config->stackmethod = GISTACKING_METHOD_MINMAX;
    }

    if (strcmp(method, "ksigma") == 0) {
        config->stackmethod = GISTACKING_METHOD_KSIGMA;
    }

    cx_free(method);

    switch (config->stackmethod) {
    case GISTACKING_METHOD_AVERAGE:
        config->min_nr_frames = MIN_IMAGES_AVERAGE;
        break;

    case GISTACKING_METHOD_MEDIAN:
        config->min_nr_frames = MIN_IMAGES_MEDIAN;
        break;

    case GISTACKING_METHOD_MINMAX:
        config->min_nr_frames = MIN_IMAGES_MINMAX;
        break;

    case GISTACKING_METHOD_KSIGMA:
        config->min_nr_frames = MIN_IMAGES_KSIGMA;
        break;

    case GISTACKING_METHOD_UNDEFINED:
    default:
        giraffe_stacking_config_destroy(config);
        config = NULL;

        cpl_error_set(fctid, CPL_ERROR_ILLEGAL_INPUT);

        break;
    }


    return config;

}


/**
 * @brief
 *   Destroys a setup structure for the stacking of images.
 *
 * @param config The setup structure to destroy.
 *
 * @return Nothing.
 *
 * The function deallocates the memory used by the setup structure
 * @em config.
 */

void
giraffe_stacking_config_destroy(GiStackingConfig *config)
{

    if (config != NULL) {
        cx_free(config);
    }

    return;
}


/**
 * @brief
 *   Adds parameters for the stacking of images.
 *
 * @param list  Parameter list to which parameters are added.
 *
 * @return Nothing.
 *
 * TBD
 */

void
giraffe_stacking_config_add(cpl_parameterlist *list)
{

    cpl_parameter *p;

    if (list == NULL) {
        return;
    }

    p = cpl_parameter_new_enum("giraffe.stacking.method",
                               CPL_TYPE_STRING,
                               "Stacking method: average, median, minmax or "
                               "ksigma",
                               "giraffe.stacking",
                               "average", 4, "average", "median",
                               "minmax", "ksigma");

    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "stack-method");
    cpl_parameterlist_append(list, p);

    p = cpl_parameter_new_value("giraffe.stacking.ksigma.low",
                                CPL_TYPE_DOUBLE,
                                "Lower threshold multiplier for method "
                                "ksigma",
                                "giraffe.stacking.ksigma",
                                5.0);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "stack-ksigmalow");
    cpl_parameterlist_append(list, p);

    p = cpl_parameter_new_value("giraffe.stacking.ksigma.high",
                                CPL_TYPE_DOUBLE,
                                "Upper threshold multiplier for method "
                                "ksigma",
                                "giraffe.stacking.ksigma",
                                5.0);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "stack-ksigmahigh");
    cpl_parameterlist_append(list, p);

    p = cpl_parameter_new_value("giraffe.stacking.minmax.minimum",
                                CPL_TYPE_INT,
                                "Minimum rejection level for method minmax",
                                "giraffe.stacking.minmax",
                                1);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "stack-minreject");
    cpl_parameterlist_append(list, p);

    p = cpl_parameter_new_value("giraffe.stacking.minmax.maximum",
                                CPL_TYPE_INT,
                                "Maximum rejection level for method minmax",
                                "giraffe.stacking.minmax",
                                1);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "stack-maxreject");
    cpl_parameterlist_append(list, p);

    return;

}
/**@}*/
