/*                                                                              *
 *   This file is part of the ESO X-Shooter package                                *
 *   Copyright (C) 2004,2005 European Southern Observatory                      *
 *                                                                              *
 *   This library 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, 51 Franklin St, Fifth Floor, Boston, MA  02111-1307  USA       *
 *                                                                              */
 
#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif


/*-----------------------------------------------------------------------------
                                Includes
 -----------------------------------------------------------------------------*/
#include <xsh_fit.h>
#include <tests.h>
#include <cpl_test.h>
#include <math.h>
#include <float.h>

/*-----------------------------------------------------------------------------
                                Defines
 -----------------------------------------------------------------------------*/

#define MODULE_ID "XSH_FIT"

/* Acceptable numerical error margin */
#define XSH_TEST_MARGIN 2.0

#define xsh_test(expr) \
 do { \
    if (!(expr)) assure(0, CPL_ERROR_ILLEGAL_OUTPUT, "Test failed"); \
 } while(0) \

#define xsh_test_tol(first, second, tolerance)                              \
    do {                                                                       \
        const double xsh_test_first = (double)(first);                      \
        const double xsh_test_second = (double)(second);                    \
        const double xsh_test_tolerance = (double)(tolerance);              \
       /* The error margin on the tolerance */                                 \
        const double xsh_test_margin = (double)(XSH_TEST_MARGIN);        \
                                                                               \
        if (!(fabs(xsh_test_first - xsh_test_second) <=                  \
            xsh_test_tolerance * xsh_test_margin))                       \
        {                                                                      \
            xsh_test(0);                                                    \
        }                                                                      \
    } while (0)

#define     IMAGESZ         10
#define     NFRAMES         10
#define     IMAGESZFIT      256

#define xsh_fit_imagelist_is_zero(A, B)              \
    xsh_fit_imagelist_is_zero_macro(A, B)
#define xsh_fit_image_is_zero(A, B)                  \
    xsh_fit_image_is_zero_macro(A, B)


/*----------------------------------------------------------------------------*/
/**
 * @defgroup xsh_fit_test Testing of the fit utilities 
 * @ingroup unit_tests
 */
/*----------------------------------------------------------------------------*/

/*-----------------------------------------------------------------------------
                            Private Function prototypes
 -----------------------------------------------------------------------------*/

static void xsh_fit_imagelist_polynomial_tests(void);
static void xsh_fit_imagelist_is_zero_macro(const cpl_imagelist *, double);
static void xsh_fit_image_is_zero_macro(const cpl_image *, double);

static cpl_error_code xsh_image_fill_noise_uniform(cpl_image *, double,
                                                      double);


/*----------------------------------------------------------------------------*/
/**
   @brief   Unit tests of fit module
**/
/*----------------------------------------------------------------------------*/


int main(void)
{
  int ret=0;

    TESTS_INIT(MODULE_ID);

    check( xsh_fit_imagelist_polynomial_tests() );

  cleanup:
    if (cpl_error_get_code() != CPL_ERROR_NONE) {
	xsh_error_dump(CPL_MSG_ERROR);
	ret= 1;
    }
    TEST_END();
    return ret;
}


static void xsh_fit_imagelist_polynomial_tests(void)
{

    const double ditval[] = {0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0};
    cpl_imagelist * fit;
    cpl_imagelist * input;
    cpl_image     * dfiterror
        = cpl_image_new(IMAGESZFIT, IMAGESZFIT, CPL_TYPE_DOUBLE);
    cpl_image     * ffiterror
        = cpl_image_new(IMAGESZFIT, IMAGESZFIT, CPL_TYPE_FLOAT);
    cpl_image     * ifiterror
        = cpl_image_new(IMAGESZFIT, IMAGESZFIT, CPL_TYPE_INT);
    const int       ndits = (int)(sizeof(ditval)/sizeof(double));
    cpl_vector    * vdit  = cpl_vector_wrap(ndits, (double*)ditval);
    const double    sqsum = 204.0; /* Sum of squares of ditvals */
    const double    mytol = 2.76 * FLT_EPSILON;
    int i;
    const cpl_type pixel_type[] = {CPL_TYPE_DOUBLE, CPL_TYPE_FLOAT, CPL_TYPE_INT};
    int ntest;


    cpl_msg_info(cpl_func, "Testing with %d %d X %d images",
                 ndits, IMAGESZFIT, IMAGESZFIT);

    fit = xsh_fit_imagelist_polynomial(NULL, NULL, 0, 0, CPL_FALSE, NULL);
    xsh_test( cpl_error_get_code() != CPL_ERROR_NONE );
    xsh_test( cpl_error_get_code() != CPL_ERROR_UNSPECIFIED );
    cpl_error_reset();
    xsh_test( fit == NULL );
    cpl_imagelist_delete(fit);

    input = cpl_imagelist_new();
    fit = xsh_fit_imagelist_polynomial(vdit, input, 0, 0, CPL_FALSE, NULL);
    xsh_test( cpl_error_get_code() != CPL_ERROR_NONE );
    xsh_test( cpl_error_get_code() != CPL_ERROR_UNSPECIFIED );
    cpl_error_reset();
    xsh_test( fit == NULL );
    cpl_imagelist_delete(fit);

    fit = xsh_fit_imagelist_polynomial(vdit, input, 1, 1, CPL_FALSE, NULL);
    xsh_test( cpl_error_get_code() != CPL_ERROR_NONE );
    xsh_test( cpl_error_get_code() != CPL_ERROR_UNSPECIFIED );
    cpl_error_reset();
    xsh_test( fit == NULL );
    cpl_imagelist_delete(fit);

    /* Test with all types of pixels */
    for (ntest = 0; ntest < 3; ntest++) {

        const cpl_type test_type = pixel_type[ntest];

        cpl_image * image = cpl_image_new(IMAGESZFIT, IMAGESZ, test_type);

        
        cpl_msg_info(cpl_func, "Fitting with pixel type %u",
                     (unsigned)test_type);

        xsh_test(!cpl_imagelist_set(input, image, 0));

        image = cpl_image_duplicate(image);

        xsh_test(!xsh_image_fill_noise_uniform(image, 1.0, 20.0));

        xsh_test(!cpl_image_multiply_scalar(image, ditval[1]));

        xsh_test(!cpl_imagelist_set(input, image, 1));

        /* A perfectly linear set */
        for (i=2; i < ndits; i++) {

            image
                = cpl_image_multiply_scalar_create(cpl_imagelist_get(input, 1),
                                                   ditval[i]);

            xsh_test(!cpl_imagelist_set(input, image, i));

        }

        fit = xsh_fit_imagelist_polynomial(vdit, input, 1, ndits-1, CPL_FALSE, NULL);

        xsh_test( cpl_error_get_code() == CPL_ERROR_NONE );
        xsh_test( cpl_imagelist_get_size(fit) == ndits - 1 );

        /* The linarity must be equal to the values in image 1
           - normalize */
        xsh_test(!cpl_image_divide(cpl_imagelist_get(fit, 0),
                                            cpl_imagelist_get(input, 1)));

        /* Subtract the expected value in the 1st image */
        xsh_test(!cpl_image_subtract_scalar(cpl_imagelist_get(fit, 0), 1.0));

        xsh_fit_imagelist_is_zero(fit, IMAGESZFIT * mytol);

        cpl_imagelist_delete(fit);
        cpl_imagelist_delete(input);
        input = cpl_imagelist_new();
    }

    /* Create a list of images with a 2nd order function */
    for (i=0; i < ndits; i++) {
        cpl_image * image = cpl_image_new(IMAGESZFIT, IMAGESZFIT,
                                          CPL_TYPE_DOUBLE);

        xsh_test(!cpl_image_add_scalar(image, ditval[i]*ditval[i]));

        xsh_test(!cpl_imagelist_set(input, image, i));

        cpl_msg_debug(cpl_func, "Dit and mean of input image no. %d: %g %g",
                      i, ditval[i], cpl_image_get_mean(image));
    }

    fit = xsh_fit_imagelist_polynomial(vdit, input, 1, ndits, CPL_FALSE,
                                          NULL);
    if (cpl_error_get_code() != CPL_ERROR_NONE ) {
        /* Fails on 32-bit intel, but not on others */
        xsh_test( cpl_error_get_code() == CPL_ERROR_SINGULAR_MATRIX );
        cpl_error_reset();
        xsh_test( fit == NULL );
    }
    cpl_imagelist_delete(fit);

    /* Illegal max-degree */
    fit = xsh_fit_imagelist_polynomial(vdit, input, 1, 0, CPL_FALSE, NULL);
    xsh_test( cpl_error_get_code() != CPL_ERROR_NONE );
    xsh_test( cpl_error_get_code() != CPL_ERROR_UNSPECIFIED );
    cpl_error_reset();
    xsh_test( fit == NULL );
    cpl_imagelist_delete(fit);

    /* Illegal min-degree */
    fit = xsh_fit_imagelist_polynomial(vdit, input, -1, 0, CPL_FALSE, NULL);
    xsh_test( cpl_error_get_code() != CPL_ERROR_NONE );
    xsh_test( cpl_error_get_code() != CPL_ERROR_UNSPECIFIED );
    cpl_error_reset();
    xsh_test( fit == NULL );
    cpl_imagelist_delete(fit);


    /* Fit with zero-order term */
    /* Also, try to use an integer-type image for fitting error */
    fit = xsh_fit_imagelist_polynomial(vdit, input, 0, 2, CPL_TRUE,
                                          ifiterror);

    xsh_test( cpl_error_get_code() == CPL_ERROR_NONE );
    xsh_test( cpl_imagelist_get_size(fit) == 3 );
    xsh_fit_image_is_zero(ifiterror, mytol); 

    xsh_test(!cpl_image_subtract_scalar(cpl_imagelist_get(fit, 2), 1.0));

    xsh_fit_imagelist_is_zero(fit, mytol);

    cpl_imagelist_delete(fit);



    /* Fit with zero-order term */
    /* Also, try to use an integer-type image for fitting error */
    fit = xsh_fit_imagelist_polynomial(vdit, input, 0, ndits-1, CPL_TRUE,
                                          ifiterror);

    xsh_test( cpl_error_get_code() == CPL_ERROR_NONE );
    xsh_test( cpl_imagelist_get_size(fit) == ndits );
    xsh_fit_image_is_zero(ifiterror, mytol);

    xsh_test(!cpl_image_subtract_scalar(cpl_imagelist_get(fit, 2), 1.0));

    xsh_fit_imagelist_is_zero(fit, mytol);

    cpl_imagelist_delete(fit);

    /* Fit without zero-order term */
    fit = xsh_fit_imagelist_polynomial(vdit, input, 1, ndits-1, CPL_FALSE,
                                          dfiterror);

    xsh_test( cpl_error_get_code() == CPL_ERROR_NONE );
    xsh_test( cpl_imagelist_get_size(fit) == ndits-1 );

    xsh_test(!cpl_image_subtract_scalar(cpl_imagelist_get(fit, 1), 1.0));

    xsh_fit_imagelist_is_zero(fit, mytol);
    xsh_fit_image_is_zero(dfiterror, mytol);

    cpl_imagelist_delete(fit);

    /* Fit with no zero- and 1st-order terms */
    fit = xsh_fit_imagelist_polynomial(vdit, input, 2, ndits, CPL_TRUE,
                                          ffiterror);

    xsh_test( cpl_error_get_code() == CPL_ERROR_NONE );
    xsh_test( cpl_imagelist_get_size(fit) == ndits-1 );

    xsh_test(!cpl_image_subtract_scalar(cpl_imagelist_get(fit, 0), 1.0));

    xsh_fit_imagelist_is_zero(fit, mytol);
    xsh_fit_image_is_zero(ffiterror, mytol);

    cpl_imagelist_delete(fit);

    /* Fit with one zero-term */
    fit = xsh_fit_imagelist_polynomial(vdit, input, 0, 0, CPL_TRUE,
                                          dfiterror);

    xsh_test( cpl_error_get_code() == CPL_ERROR_NONE );
    xsh_test( cpl_imagelist_get_size(fit) == 1 );

    xsh_test(!cpl_image_subtract_scalar(cpl_imagelist_get(fit, 0),
                                           sqsum/(double)ndits));

    xsh_fit_imagelist_is_zero(fit, mytol);

    cpl_imagelist_delete(fit);

    cpl_imagelist_delete(input);

    (void)cpl_vector_unwrap(vdit);

    /* Try to fit as many coefficients are there are data points */
    /* Also, use floats this time */

    input = cpl_imagelist_new();

    for (ntest = 1; ntest <= ndits; ntest++) {
        const double gain = 4.0; /* Some random number */

        cpl_msg_info(cpl_func, "Fitting %d coefficients to as many points",
                     ntest);

        vdit  = cpl_vector_wrap(ntest, (double*)ditval);

        /* Create a list of images with a 2nd order function */
        for (i = ntest - 1; i < ntest; i++) {
            cpl_image * image = cpl_image_new(IMAGESZFIT, IMAGESZFIT,
                                              CPL_TYPE_FLOAT);

            xsh_test(!cpl_image_add_scalar(image, gain * ditval[i]*ditval[i]));

            xsh_test(!cpl_imagelist_set(input, image, i));

            cpl_msg_debug(cpl_func, "Dit and mean of input image no. %d: %g %g",
                          i, ditval[i], cpl_image_get_mean(image));
        }

        /* Ready for fitting */

        /* Fit with zero-order term */
        fit = xsh_fit_imagelist_polynomial(vdit, input, 0, ntest-1, CPL_TRUE,
                                              ffiterror);

        (void)cpl_vector_unwrap(vdit);

        if (cpl_error_get_code() != CPL_ERROR_NONE) {
            xsh_test( fit == NULL );

            cpl_msg_info(cpl_func, "Could not fit %d coefficients to as many "
                         "points", ntest);

            xsh_test( cpl_error_get_code() != CPL_ERROR_UNSPECIFIED );
            cpl_error_reset();

            break;
        }

        xsh_test( cpl_imagelist_get_size(fit) == ntest );

        if (ntest == 2) {
            xsh_test(!cpl_image_subtract_scalar(cpl_imagelist_get(fit, 1),
                                                   gain));
        } else if (ntest > 2) {
            xsh_test(!cpl_image_subtract_scalar(cpl_imagelist_get(fit, 2),
                                                   gain));
        }

        xsh_fit_imagelist_is_zero(fit, mytol);

        xsh_fit_image_is_zero(ffiterror, mytol);

        cpl_imagelist_delete(fit);
    }

    cpl_imagelist_delete(input);

    /* Done testing */
    cpl_image_delete(dfiterror);
    cpl_image_delete(ffiterror);
    cpl_image_delete(ifiterror);

  cleanup:
    return;
}



/*----------------------------------------------------------------------------*/
/**
  @brief  Verify that all elements in an imagelist are zero (within a tolerance)
  @param  self    The list of images to check
  @param  tol     The non-negative tolerance
  @return void

 */
/*----------------------------------------------------------------------------*/
static void xsh_fit_imagelist_is_zero_macro(const cpl_imagelist * self,
                                               double tol)
{

    const int n = cpl_imagelist_get_size(self);
    int i;

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

	check(
	  xsh_fit_image_is_zero_macro(cpl_imagelist_get_const( self, i), 
          tol));
	
    }
  cleanup:
    return;
}


/*----------------------------------------------------------------------------*/
/**
  @brief  Verify that all elements in an image are zero (within a tolerance)
  @param  self    The image to check
  @param  tol     The non-negative tolerance
  param   line    The line number of the caller
  @return void

 */
/*----------------------------------------------------------------------------*/
static void xsh_fit_image_is_zero_macro(const cpl_image * self, double tol)
{

    cpl_stats * stats = cpl_stats_new_from_image(self,
                                                 CPL_STATS_MIN | CPL_STATS_MAX
                                                 | CPL_STATS_MEAN);

    const double mymin = cpl_stats_get_min(stats);
    const double mymax = cpl_stats_get_max(stats);

    xsh_test_tol( mymin, 0.0, tol );
    xsh_test_tol( mymax, 0.0, tol );

  cleanup:
    cpl_stats_delete(stats);
    return;

}


/*----------------------------------------------------------------------------*/
/**
  @brief    Fill an image with uniform random noise distribution.
  @param    self        Image to fill
  @param    min_pix     Minimum output pixel value.
  @param    max_pix     Maximum output pixel value.
  @return   CPL_ERROR_NONE or the relevant CPL error code on error.
  @see cpl_image_fill_noise_uniform()
  @note Images with pixel type int are also supported

  FIXME: Add Integer support to cpl_image_fill_noise_uniform()
  
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code xsh_image_fill_noise_uniform(cpl_image * self,
                                                      double min_pix,
                                                      double max_pix)
{

    cpl_ensure_code(self != NULL, CPL_ERROR_NULL_INPUT);

    if (cpl_image_get_type(self) == CPL_TYPE_INT) {

        int       * pi = cpl_image_get_data_int(self);
        const int   nx = cpl_image_get_size_x(self);
        const int   ny = cpl_image_get_size_y(self);
        int         i, j;
        

        for (j=0 ; j < ny ; j++) {
            for (i=0 ; i < nx ; i++) {
                const double value
                    = min_pix + (max_pix * (double)rand())/(double)RAND_MAX;

                pi[i + j * nx] = (int) value;
            }
        }
    } else {

        const cpl_error_code error
            = cpl_image_fill_noise_uniform(self, min_pix, max_pix);
        cpl_ensure_code(!error, error);
    }

    return CPL_ERROR_NONE;
}
