/*
 * 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 <string.h>
#include <math.h>

#include <cxlist.h>

#include "gialias.h"
#include "giarray.h"
#include "girange.h"
#include "giwindow.h"
#include "gidark.h"


/**
 * @defgroup gidark Dark correction
 *
 * TBD
 */

/**@{*/

inline static cxint
_giraffe_halfrange_mode(cxdouble *mode, cxdouble* array, cxsize* size,
                        cxdouble portion, cxdouble epsilon)
{

    cxint status = 0;

    register cxsize i = 0;

    cxsize ndata = 0;
    cxsize _size = *size;

    cxdouble tiny = CX_MAXDOUBLE;


    for (i = 0; i < _size - 1; i++) {

        cxdouble t = array[i + 1] - array[i];

        if (t <= 0.) {
            continue;
        }

        if (t < tiny) {
            tiny = t;
        }
    }

    tiny /= 2.;

    if (tiny <= epsilon) {
        tiny = epsilon;
        //        return -1;
    }

    if (_size < 3) {

        cxdouble _mode = 0.;


        status = 0;

        switch (_size) {
        case 2:
            _mode = (array[0] + array[1]) / 2.;
            break;

        case 1:
            _mode = array[0];
            break;

        default:
            status = -2;
            break;
        }

        *mode = _mode;
        return status;

    }
    else {

        cxsize count = 0;

        cxdouble iwidth = array[_size - 1] - array[0];
        cxdouble iweps = 0.;

        cx_list* intervals = NULL;

        GiRange* interval = NULL;


        if (iwidth <= epsilon) {
            *mode = giraffe_array_mean(array, _size);
            return 0;
        }

        iwidth *= portion;
        iweps = iwidth + tiny;

        intervals = cx_list_new();

        i = 0;
        while (i < _size) {

            register cxsize j = 0;
            register cxssize k = -1;

            cxsize nvalues = 0;

            GiRange* _interval = giraffe_range_create(array[i] - tiny,
                                                      array[i] + iweps);


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

                register cxdouble value = array[j];


                if (value >= giraffe_range_get_max(_interval)) {
                    break;
                }

                if (value <= giraffe_range_get_min(_interval)) {
                    continue;
                }

                if (k < 0) {
                    giraffe_range_set_min(_interval, value - tiny);
                    k = j;
                }

            }

            giraffe_range_set_max(_interval, array[j - 1] + tiny);
            nvalues = j - k;

            if (nvalues > count) {

                cx_list_iterator position = cx_list_begin(intervals);
                cx_list_const_iterator last = cx_list_end(intervals);

                count = nvalues;

                while (position != last) {
                    position =
                        cx_list_erase(intervals, position,
                                      (cx_free_func)giraffe_range_delete);
                }
                cx_list_clear(intervals);

                cx_list_push_back(intervals, _interval);
                _interval = NULL;

            }
            else if (nvalues == count) {

                cx_list_push_back(intervals, _interval);
                _interval = NULL;

            }
            else {

                giraffe_range_delete(_interval);
                _interval = NULL;

            }

            ++i;

        }

        if (cx_list_size(intervals) == 1) {

            GiRange* _interval = cx_list_front(intervals);

            cxdouble minimum = giraffe_range_get_min(_interval);
            cxdouble maximum = giraffe_range_get_max(_interval);

            interval = giraffe_range_create(minimum, maximum);

        }
        else {

            cxdouble minimum = 0.;
            cxdouble maximum = 0.;

            cx_list_iterator position = cx_list_begin(intervals);

            cx_list_const_iterator last = cx_list_end(intervals);


            iwidth = CX_MAXDOUBLE;


            /*
             * Find smallest intervals
             */

            while (position != last) {

                GiRange* _interval = cx_list_get(intervals, position);

                cxdouble t = giraffe_range_get_max(_interval) -
                    giraffe_range_get_min(_interval);


                if (t <= 0.) {
                    continue;
                }

                if (t < iwidth) {
                    iwidth = t;
                }

                position = cx_list_next(intervals, position);

            }

            iwidth += tiny;


            /*
             * Remove intervals with a larger width than iwidth.
             */

            position = cx_list_begin(intervals);
            last = cx_list_end(intervals);

            while (position != last) {

                GiRange* _interval = cx_list_get(intervals, position);

                cxdouble t = giraffe_range_get_max(_interval) -
                    giraffe_range_get_min(_interval);


                if (t >= iwidth) {
                    position =
                        cx_list_erase(intervals, position,
                                      (cx_free_func)giraffe_range_delete);
                }
                else {
                    position = cx_list_next(intervals, position);
                }

            }

            minimum = giraffe_range_get_min(cx_list_front(intervals));
            maximum = giraffe_range_get_max(cx_list_back(intervals));

            interval = giraffe_range_create(minimum, maximum);

        }

        cx_list_destroy(intervals, (cx_free_func)giraffe_range_delete);
        intervals = NULL;


        /*
         * Discard data outside the intervals
         */

        ndata = 0;

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

            if (array[i] < giraffe_range_get_min(interval)) {
                continue;
            }

            if (array[i] > giraffe_range_get_max(interval)) {
                break;
            }

            array[ndata++] = array[i];

        }

        giraffe_range_delete(interval);
        interval = NULL;


        if (ndata == _size) {

            cxdouble start = array[1] - array[0];
            cxdouble end = array[ndata - 1] - array[ndata - 2];


            if (fabs(start - end) < epsilon) {

                /* Remove the first and the last value */
                ndata -=2;
                memmove(array, &array[1], ndata * sizeof(cxdouble));

            }
            else {

                if (start < end) {

                    /* Remove last value */
                    --ndata;

                }
                else {

                    /* Remove first value */
                    --ndata;
                    memmove(array, &array[1], ndata * sizeof(cxdouble));

                }

            }

        }

        *size = ndata;

        status = _giraffe_halfrange_mode(mode, array, size, portion, epsilon);

    }

    return status;

}


inline static cxdouble
_giraffe_dark_compute_mode(const cpl_image* image, const cpl_image* bpixel)
{

    register cxsize i = 0;

    cxint status = 0;

    cxsize ndata = cpl_image_get_size_x(image) * cpl_image_get_size_y(image);
    cxsize count = 0;
    cxsize nbuffer = 0;

    const cxdouble* _image = cpl_image_get_data_double_const(image);

    cxdouble mode = 0.;
    cxdouble delta = 0.;
    cxdouble* buffer = NULL;
    cxdouble* sorted_image = NULL;


    cx_assert(cpl_image_get_type(image) == CPL_TYPE_DOUBLE);

    sorted_image = cx_calloc(ndata, sizeof(cxdouble));
    memcpy(sorted_image, _image, ndata * sizeof(cxdouble));

    if (bpixel != NULL) {

        const cxint* _bpixel = cpl_image_get_data_int_const(bpixel);


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

            if (_bpixel[i] == GI_BPIX_OK) {
                sorted_image[count++] = _image[i];
            }

        }

    }
    else {
        count = ndata;
    }

    status = giraffe_array_sort(sorted_image, count);

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


    buffer = cx_calloc(count, sizeof(cxdouble));

    for (i = 0; i < count; i += 1000) {
        buffer[nbuffer++] = sorted_image[i];
    }


    // FIXME: Check whether _giraffe_halfrange_mode() needs to update the
    //        size of buffer on return!

    status = _giraffe_halfrange_mode(&mode, buffer, &nbuffer, 0.1, 1.e-9);

    delta = CX_MIN(mode / 10.,
                   (sorted_image[count - 1] - sorted_image[0]) / 2.);
    nbuffer = count;

    while ((nbuffer > 50000) && (delta > 1.e-6)) {

        register cxsize j = 0;

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

            if (sorted_image[i] < (mode - delta)) {
                continue;
            }

            if (sorted_image[i] > (mode + delta)) {
                break;
            }

            buffer[j++] = sorted_image[i];

        }

        delta /= 2.;

        if (j > 0) {
            nbuffer = j;
        }

    }

    if (delta > 1.e-6) {
        status = _giraffe_halfrange_mode(&mode, buffer, &nbuffer, 0.5, 1.e-9);
    }
    else {
        mode = giraffe_array_mean(buffer, nbuffer);
    }

    cx_free(buffer);
    buffer = NULL;

    cx_free(sorted_image);
    sorted_image = NULL;

    return mode;

}


/**
 * @brief
 *  Subtract the dark current from a bias corrected image.
 *
 * @param image   The bias subtracted image to correct.
 * @param dark    The dark image to use for the correction.
 * @param bpixel  Bad pixel map.
 * @param data    Dark correction results data container.
 * @param config  Dark correction setup parameters.
 *
 * @return
 *   The function returns 0 on success, and a non-zero value otherwise.
 *
 * TBD
 */

cxint
giraffe_subtract_dark(GiImage* image, const GiImage* dark,
                      const GiImage* bpixel, GiDarkResults* data,
                      const GiDarkConfig* config)
{

    cxbool crop = FALSE;

    cxint nx = 0;
    cxint ny = 0;

    cxdouble exptime = 0.;
    cxdouble darktime = 0.;
    cxdouble dark_max = 0.;
    cxdouble dark_mode = 0.;
    cxdouble dark_value = 0.;
    cxdouble timescale = 1.;

    cpl_propertylist* properties = NULL;

    const cpl_image* _dark = NULL;
    const cpl_image* _bpixel = NULL;

    cpl_image* _image = NULL;


    if ((image == NULL) || (dark == NULL)) {
        return -1;
    }

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

    _image = giraffe_image_get(image);
    _dark = giraffe_image_get(dark);

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

    if ((nx != cpl_image_get_size_y(_dark)) ||
        (ny != cpl_image_get_size_x(_dark))) {
            return -3;
    }

    if (bpixel != NULL) {

        GiWindow area = {1, 1, ny, nx};

        properties = giraffe_image_get_properties(bpixel);
        _bpixel = giraffe_image_get(bpixel);

        if (cpl_propertylist_has(properties, GIALIAS_PRSCX) == TRUE) {
            area.x0 += cpl_propertylist_get_int(properties, GIALIAS_PRSCX);
            crop = TRUE;
        }

        if (cpl_propertylist_has(properties, GIALIAS_PRSCY) == TRUE) {
            area.y0 += cpl_propertylist_get_int(properties, GIALIAS_PRSCY);
            crop = TRUE;
        }

        if (cpl_propertylist_has(properties, GIALIAS_OVSCX) == TRUE) {
            area.x1 = cpl_image_get_size_x(_bpixel) -
                cpl_propertylist_get_int(properties, GIALIAS_OVSCX);
            crop = TRUE;
        }

        if (cpl_propertylist_has(properties, GIALIAS_OVSCY) == TRUE) {
            area.y1 = cpl_image_get_size_y(_bpixel) -
                cpl_propertylist_get_int(properties, GIALIAS_OVSCY);
            crop = TRUE;
        }

        if (crop == TRUE) {
            _bpixel = cpl_image_extract(_bpixel, area.x0, area.y0,
                                        area.x1, area.y1);
        }

    }


    /*
     * Get the dark current time from the input image and the dark.
     */

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

    if (cpl_propertylist_has(properties, GIALIAS_EXPTIME) == FALSE) {
        return 1;
    }
    else {
        exptime = cpl_propertylist_get_double(properties, GIALIAS_EXPTIME);
    }

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

    if (cpl_propertylist_has(properties, GIALIAS_EXPTIME) == FALSE) {
        return 1;
    }
    else {
        darktime = cpl_propertylist_get_double(properties, GIALIAS_EXPTIME);
    }


    /*
     * Dark current scale factor
     */

    timescale = exptime / darktime;


    /*
     * Compute mode and maximum of the dark.
     */

    dark_max = cpl_image_get_max(_dark) * timescale;
    dark_mode = _giraffe_dark_compute_mode(_dark, _bpixel) * timescale;


    /*
     * Correct the input image for the dark current
     */

    switch (config->method) {
    case GIDARK_METHOD_UNIFORM:

        if (dark_max < config->threshold) {

            /* No dark subtraction */

            dark_value = 0.;

        }
        else {

            dark_value = dark_mode;
            cpl_image_subtract_scalar(_image, dark_value);

        }
        break;

    case GIDARK_METHOD_ZMASTER:
        {

            register cxint i = 0;

            cxdouble* pximage = NULL;
            cxdouble* pxdark = NULL;

            cpl_image* scaled_dark = cpl_image_duplicate(_dark);

            pximage = cpl_image_get_data_double(_image);
            pxdark = cpl_image_get_data_double(scaled_dark);

            if (_bpixel == NULL) {

                register cxint j = 0;
                register cxint n = nx * ny;

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

                    pxdark[j] *= timescale;

                    if (pxdark[j] < config->threshold) {
                        pxdark[j] = dark_mode;
                    }

                }

            }
            else {

                register cxint j = 0;
                register cxint n = nx * ny;

                const cxint* pxmask = cpl_image_get_data_int_const(_bpixel);


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

                    if ((pxmask[j] & GI_M_PIX_SET) == 0x0) {
                        pxdark[j] *= timescale;
                    }
                    else {
                        pxdark[j] = dark_mode;
                    }

                }

            }


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

                register cxint j = 0;
                register cxint base = i * ny;

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

                    register cxint offset = base + j;

                    pximage[offset] -= pxdark[offset];

                }

            }

            dark_mode = _giraffe_dark_compute_mode(scaled_dark, _bpixel);
            dark_value = dark_mode;

            cpl_image_delete(scaled_dark);
            scaled_dark = NULL;

        }
        break;

    case GIDARK_METHOD_MASTER:
    default:
        {

            register cxint i = 0;

            const cxdouble* pxdark = NULL;

            cxdouble* pximage = NULL;

            pximage = cpl_image_get_data_double(_image);
            pxdark = cpl_image_get_data_double_const(_dark);

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

                register cxint j = 0;
                register cxint base = i * ny;

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

                    register cxint offset = base + j;

                    pximage[offset] -= pxdark[offset] * timescale;

                }

            }
        }
        break;

    }


    /*
     * Update the properties of the corrected image.
     */

    properties = giraffe_image_get_properties(image);

    cpl_propertylist_update_double(properties, GIALIAS_DARKVALUE,
                                   dark_value / timescale);
    cpl_propertylist_set_comment(properties, GIALIAS_DARKVALUE,
                                 "Used dark current [ADU/s]");
    cpl_propertylist_update_double(properties, GIALIAS_DARKEXPECT,
                                   dark_mode / timescale);
    cpl_propertylist_set_comment(properties, GIALIAS_DARKEXPECT,
                                 "Expected dark current [ADU/s]");


    /*
     * Update dark results data
     */

    if (data != NULL) {
        data->value = dark_value;
        data->expected = dark_mode;
        data->mode = dark_mode / timescale;
        data->maximum = dark_max / timescale;
    }


    /*
     * Cleanup
     */

    if (crop == TRUE) {
        cpl_image_delete((cpl_image*)_bpixel);
        _bpixel = NULL;
    }


    return 0;

}
/**@}*/
