/*                                                                              *
 *   This file is part of the ESO VISIR 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 <cpl.h>

#include <visir_utils.h>
#include <string.h>
#include <stdlib.h>

#include "visir_serialize.c"

/*----------------------------------------------------------------------------*/
/**
 * @defgroup visir_utils_test Testing of the VISIR utilities
 */
/*----------------------------------------------------------------------------*/

#define CONCAT(a,b) a ## _ ## b
#define CONCAT2X(a,b) CONCAT(a,b)

#define PIXEL_TYPE double
#define STDEV_TYPE CPL_TYPE_DOUBLE
#define PIXEL_TYPE_CPL CPL_TYPE_DOUBLE
static int prnok_nz = 0;
static double * prnok = NULL;
#include "../recipes/visir_util_clip_body.c"
#undef PIXEL_TYPE
#undef STDEV_TYPE
#undef PIXEL_TYPE_CPL

/* column major */
#define IND(x,y,nx) ((x) + (y) * (nx))

size_t visir_get_next_regular(size_t a);

static void visir_serialize_test(void);
static void visir_macro_test(void);
static void visir_bound_test(void);
static void visir_get_kth_test(void);
static void visir_clip_kappa_sigma_test(void);
static void visir_fftxcorrelate_test(void);
static void visir_interpolate_rejected_test(void);
static void visir_num_threads_test(void);
static void visir_mean_fast_test(void);
static void visir_next_regular_test(void);
static void visir_test_parallel_median(void);
static void visir_test_linintp_values(void);
#include <cxlist.h>
#include <stdlib.h>

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

#define LEN(a) sizeof((a))/sizeof((a)[0])

int main(void)
{

    cpl_test_init(PACKAGE_BUGREPORT, CPL_MSG_WARNING);
    visir_serialize_test();
    visir_macro_test();
    visir_bound_test();
    visir_get_kth_test();
    visir_clip_kappa_sigma_test();
    visir_fftxcorrelate_test();
    visir_interpolate_rejected_test();
    visir_num_threads_test();
    visir_mean_fast_test();
    visir_next_regular_test();
    visir_test_parallel_median();
    visir_test_linintp_values();

    return cpl_test_end(0);
}


static void visir_num_threads_test(void)
{
    FILE          * stream;

    stream = cpl_msg_get_level() > CPL_MSG_INFO
        ? fopen("/dev/null", "a") : stdout;

    {
        const char * orig = getenv("OMP_NUM_THREADS");
        unsetenv("OMP_NUM_THREADS");
        cpl_test_lt(0, visir_get_num_threads(CPL_FALSE));
        unsetenv("OMP_NUM_THREADS");
        cpl_test_lt(0, visir_get_num_threads(CPL_FALSE));
        setenv("OMP_NUM_THREADS", "1", 1);
        cpl_test_eq(visir_get_num_threads(CPL_FALSE), 1);
        setenv("OMP_NUM_THREADS", "3", 1);
        cpl_test_eq(visir_get_num_threads(CPL_FALSE), 3);
        setenv("OMP_NUM_THREADS", "-3", 1);
        cpl_test_eq(visir_get_num_threads(CPL_FALSE), 1);
        setenv("OMP_NUM_THREADS", "4ghdsg", 1);
        cpl_test_eq(visir_get_num_threads(CPL_FALSE), 4);
        setenv("OMP_NUM_THREADS", "ghdsg", 1);
        cpl_test_eq(visir_get_num_threads(CPL_FALSE), 1);
        setenv("OMP_NUM_THREADS", "490348563984693763894", 1);
        cpl_test_lt(0, visir_get_num_threads(CPL_FALSE));
        setenv("OMP_NUM_THREADS", "20000", 1);
        cpl_test_lt(0, visir_get_num_threads(CPL_TRUE));
        cpl_test_lt(visir_get_num_threads(CPL_TRUE), 20000);
        if (orig)
            setenv("OMP_NUM_THREADS", orig, 1);
        else
            unsetenv("OMP_NUM_THREADS");
    }

    cpl_test_nonnull( stream );
    if (stream != stdout) cpl_test_zero( fclose(stream) );
}

static void visir_serialize_test(void)
{
    FILE          * stream;

    stream = cpl_msg_get_level() > CPL_MSG_INFO
        ? fopen("/dev/null", "a") : stdout;

    {
        char * block = cpl_malloc(5);
        visir_stream * wstream = visir_stream_new(block, 5);
        serialize_string(wstream, "12345");
        serialize_uint32(wstream, 12345);
        visir_stream * rstream =
            visir_stream_new(wstream->base_buffer, 0);
        cpl_free(wstream);
        char * rs = deserialize_string(rstream);
        int ri = deserialize_uint32(rstream);
        cpl_test(strcmp(rs, "12345") == 0);
        cpl_test_eq(ri, 12345);
        cpl_free(rs);
        cpl_free(rstream->base_buffer);
        cpl_free(rstream);
    }

    {
        cpl_frameset * frames = cpl_frameset_new();
        size_t size;
        char * s = visir_frameset_serialize(frames, NULL);
        cpl_test_eq_ptr(s, NULL);
        cpl_test_error(CPL_ERROR_NULL_INPUT);

        s = visir_frameset_serialize(frames, &size);
        cpl_frameset * d = visir_frameset_deserialize(s, NULL);
        cpl_test_eq(cpl_frameset_get_size(frames), cpl_frameset_get_size(d));
        cpl_frameset_delete(d);
        cpl_free(s);


        for (int i = 0; i < 17; i++) {
            cpl_frame * frm = cpl_frame_new();
            cpl_frame_set_tag(frm, "test");
            cpl_frame_set_group(frm, CPL_FRAME_GROUP_PRODUCT);
            cpl_frame_set_level(frm, CPL_FRAME_LEVEL_FINAL);
            cpl_frame_set_type(frm, CPL_FRAME_TYPE_TABLE);
            cpl_frame_set_filename(frm, "filename.fits");
            cpl_frameset_insert(frames, frm);

            frm = cpl_frame_new();
            cpl_frame_set_tag(frm, "tsdfasfijiotw3ty");
            cpl_frame_set_group(frm, CPL_FRAME_GROUP_PRODUCT);
            cpl_frame_set_level(frm, CPL_FRAME_LEVEL_INTERMEDIATE);
            cpl_frame_set_type(frm, CPL_FRAME_TYPE_IMAGE);
            cpl_frame_set_filename(frm, "folder/fiename.fits");
            cpl_frameset_insert(frames, frm);

            frm = cpl_frame_new();
            cpl_frame_set_tag(frm, "TSDFASFIJIOTW3TY");
            cpl_frame_set_group(frm, CPL_FRAME_GROUP_RAW);
            cpl_frame_set_level(frm, CPL_FRAME_LEVEL_TEMPORARY);
            cpl_frame_set_type(frm, CPL_FRAME_TYPE_PAF);
            cpl_frame_set_filename(frm, "/folder/fiename.fits");
            cpl_frameset_insert(frames, frm);
        }

        s = visir_frameset_serialize(frames, &size);
        cpl_test_lt(0, size);
        size_t size2;
        d = visir_frameset_deserialize(s, &size2);
        cpl_test_eq(cpl_frameset_get_size(frames), cpl_frameset_get_size(d));
        cpl_test_eq(size, size2);

        for (cpl_size i = 0; i < cpl_frameset_get_size(d); i++) {
            const cpl_frame * nfrm = cpl_frameset_get_position_const(d, i);
            const cpl_frame * ofrm = cpl_frameset_get_position_const(frames, i);
            cpl_test_eq(cpl_frame_get_group(ofrm), cpl_frame_get_group(nfrm));
            cpl_test_eq(cpl_frame_get_level(ofrm), cpl_frame_get_level(nfrm));
            cpl_test_eq(cpl_frame_get_type(ofrm), cpl_frame_get_type(nfrm));
            cpl_test_zero(strcmp(cpl_frame_get_tag(ofrm),
                                 cpl_frame_get_tag(nfrm)));
            cpl_test_zero(strcmp(cpl_frame_get_filename(ofrm),
                                 cpl_frame_get_filename(nfrm)));
        }

        cpl_free(s);
        cpl_frameset_delete(frames);
        cpl_frameset_delete(d);
    }

    cpl_test_nonnull( stream );

    if (stream != stdout) cpl_test_zero( fclose(stream) );

}

static void visir_macro_test(void)
{
    FILE          * stream;

    stream = cpl_msg_get_level() > CPL_MSG_INFO
        ? fopen("/dev/null", "a") : stdout;

    {
        double a[10];
        size_t i = 0;

        cx_list * l = cx_list_new();
        for (i = 0; i < LEN(a); i++)
            cx_list_push_back(l, (void*)&a[i]);

        i = 0;
        FOR_EACH(it, l)
            cpl_test_eq_ptr(&a[i++], cx_list_get(l, it));

        cpl_test_eq(i, LEN(a));
        cpl_test_eq((size_t)cx_list_size(l), LEN(a));

        i = 0;
        FOR_EACH_T(void * p, l)
            cpl_test_eq_ptr(&a[i++], p);

        cpl_test_eq(i, LEN(a));
        cpl_test_eq((size_t)cx_list_size(l), LEN(a));

        cx_list_delete(l);
    }

    cpl_test_nonnull( stream );

    if (stream != stdout) cpl_test_zero( fclose(stream) );

}


static void visir_bound_test(void)
{
    FILE          * stream;

    stream = cpl_msg_get_level() > CPL_MSG_INFO
        ? fopen("/dev/null", "a") : stdout;

    {
        cpl_vector * v = cpl_vector_new(10);
        cpl_vector_fill(v, 0.);
        cpl_test_eq(visir_lower_bound(v, 0.), 0);
        cpl_test_eq(visir_upper_bound(v, 0.), 10);
        cpl_vector_delete(v);
    }
{
        double d[] = {1., 1., 2., 3., 3., 4., 5., 5.};
        cpl_vector * v = cpl_vector_wrap(LEN(d), d);
        cpl_test_eq(visir_lower_bound(v, 0.), 0);
        cpl_test_eq(visir_lower_bound(v, 1.), 0);
        cpl_test_eq(visir_lower_bound(v, 1.1), 2);
        cpl_test_eq(visir_lower_bound(v, 2.), 2);
        cpl_test_eq(visir_lower_bound(v, 2.1), 3);
        cpl_test_eq(visir_lower_bound(v, 5.), LEN(d) - 2);
        cpl_test_eq(visir_lower_bound(v, 6.), LEN(d));

        cpl_test_eq(visir_upper_bound(v, 0.), 0);
        cpl_test_eq(visir_upper_bound(v, 1.), 2);
        cpl_test_eq(visir_upper_bound(v, 1.1), 2);
        cpl_test_eq(visir_upper_bound(v, 2.), 3);
        cpl_test_eq(visir_upper_bound(v, 2.1), 3);
        cpl_test_eq(visir_upper_bound(v, 4.), LEN(d) - 2);
        cpl_test_eq(visir_upper_bound(v, 5.), LEN(d));
        cpl_test_eq(visir_upper_bound(v, 6.), LEN(d));
        cpl_vector_unwrap(v);
    }

    cpl_test_nonnull( stream );

    if (stream != stdout) cpl_test_zero( fclose(stream) );

}

static void visir_get_kth_test(void)
{
    FILE          * stream;
    double res = -1;

    stream = cpl_msg_get_level() > CPL_MSG_INFO
        ? fopen("/dev/null", "a") : stdout;

    {
        double a[10];

        for (size_t i = 0; i < LEN(a); i++)
            a[i] = 0;
        for (size_t i = 0; i < LEN(a); i++) {
            res = cpl_tools_get_kth_double(a, LEN(a), i);
            cpl_test_eq(0, res);
        }

        for (size_t i = 0; i < LEN(a); i++)
            a[i] = i;
        for (size_t i = 0; i < LEN(a); i++) {
            res = cpl_tools_get_kth_double(a, LEN(a), i);
            cpl_test_eq(i, res);
        }

        for (size_t i = 0; i < LEN(a); i++)
            a[i] = -(int)i;
        for (size_t i = 0; i < LEN(a); i++) {
            res = cpl_tools_get_kth_double(a, LEN(a), i);
            cpl_test_eq((double)i - LEN(a) + 1., res);
        }
    }

    {
        double a[] = {3,1,4,2,0,9,7,8,6,5};
        res = cpl_tools_get_kth_double(a, LEN(a), 4);
        cpl_test_eq(4, res);
        for (size_t i = 0; i < LEN(a) / 2; i++)
            cpl_test_leq(a[i], 4);
        for (size_t i = LEN(a)/2; i < LEN(a); i++)
            cpl_test_lt(4, a[i]);
    }

    {
        double a[] = {8,5,9,2,0,4,6,3,7,1};
        res = cpl_tools_get_kth_double(a, LEN(a), 2);
        cpl_test_eq(2, res);
        res = cpl_tools_get_kth_double(a, LEN(a), 7);
        cpl_test_eq(7, res);

        cpl_test_eq(9, a[LEN(a) - 1]);
        cpl_test_eq(8, a[LEN(a) - 2]);
        cpl_test_eq(0, a[0]);
        cpl_test_eq(1, a[1]);
    }

    {
        double a[] = {8,5,9,2,0,4,6,3,7,1};
        res = cpl_tools_get_kth_double(a, LEN(a), 7);
        cpl_test_eq(7, res);
        res = cpl_tools_get_kth_double(a, LEN(a), 2);
        cpl_test_eq(2, res);

        cpl_test_eq(9, a[LEN(a) - 1]);
        cpl_test_eq(8, a[LEN(a) - 2]);
        cpl_test_eq(0, a[0]);
        cpl_test_eq(1, a[1]);
    }

    cpl_test_nonnull( stream );

    if (stream != stdout) cpl_test_zero( fclose(stream) );

}

static void visir_clip_kappa_sigma_test(void)
{
    FILE          * stream;
    int dump;

    stream = cpl_msg_get_level() > CPL_MSG_INFO
        ? fopen("/dev/null", "a") : stdout;

    cpl_test_nonnull( stream );

    {
        double * pixels = cpl_calloc(9, sizeof(double));
        int shifts[3 * 2] = {0};
        pixels[1 + 3 * 1] = 1;

        cpl_imagelist * list = cpl_imagelist_new();
        cpl_imagelist * dev = cpl_imagelist_new();

        cpl_image* img = cpl_image_wrap_double(3, 3, pixels);
        cpl_imagelist_set(list, img, 0);
        img = cpl_image_duplicate(img);
        cpl_imagelist_set(list, img, 1);
        img = cpl_image_duplicate(img);
        cpl_imagelist_set(list, img, 2);

        visir_util_clip_kappa_sigma_double(list, dev, 1.0, 3, 3, shifts);

        cpl_image * map = cpl_image_new_from_accepted(list);
        cpl_test_eq(3, cpl_image_get(map, 2, 2, &dump));

        cpl_image_delete(map);
        cpl_imagelist_delete(list);
        cpl_imagelist_delete(dev);
    }


    {
        double * pixels = cpl_calloc(9, sizeof(double));
        /* gaus mean 100 sigma 5, 2 sigma range */
        int values[] = {92, 93, 94, 94, 95, 95, 96, 96, 96, 97,
                        97, 97, 97, 98, 98, 98, 98, 99, 99, 99,
                        99, 100, 100, 100, 100, 100, 101, 101, 101, 101,
                        102, 102, 102, 102, 103, 103, 103, 103, 104, 104,
                        104, 105, 105, 106, 106, 107, 108 };
        int shifts[LEN(values) * 2] = {0};

        cpl_imagelist * list = cpl_imagelist_new();
        cpl_imagelist * dev = cpl_imagelist_new();

        cpl_image* img = cpl_image_wrap_double(3, 3, pixels);
        for (size_t i = 0; i < LEN(values); i++) {
            cpl_image_set(img, 2, 2, values[i]);
            cpl_imagelist_set(list, img, i);
            img = cpl_image_duplicate(img);
        }
        cpl_image_delete(img);

        visir_util_clip_kappa_sigma_double(list, dev, 1.0, 3, 3, shifts);

        cpl_image * map = cpl_image_new_from_accepted(list);
        cpl_test_eq(LEN(values), cpl_image_get(map, 2, 2, &dump));

        cpl_image_delete(map);
        cpl_imagelist_delete(list);
        cpl_imagelist_delete(dev);
    }

    {
        double * pixels = cpl_calloc(9, sizeof(double));
        /* gaus mean 100 sigma 5, 2 sigma range, 2 outliers */
        int values[] = {1, 150, 94, 94, 95, 95, 96, 96, 96, 97,
                        97, 97, 97, 98, 98, 98, 98, 99, 99, 99,
                        99, 100, 100, 100, 100, 100, 101, 101, 101, 101,
                        102, 102, 102, 102, 103, 103, 103, 103, 104, 104,
                        104, 105, 105, 106, 106, 107, 108 };
        int shifts[LEN(values) * 2] = {0};

        cpl_imagelist * list = cpl_imagelist_new();
        cpl_imagelist * dev = cpl_imagelist_new();

        cpl_image* img = cpl_image_wrap_double(3, 3, pixels);
        for (size_t i = 0; i < LEN(values); i++) {
            cpl_image_set(img, 2, 2, values[i]);
            cpl_imagelist_set(list, img, i);
            img = cpl_image_duplicate(img);
        }
        cpl_image_delete(img);

        visir_util_clip_kappa_sigma_double(list, dev, 1.0, 3, 3, shifts);

        cpl_image * map = cpl_image_new_from_accepted(list);
        //cpl_image_dump_structure(map, stdout);
        //cpl_image_dump_window(map, 1, 1, 3, 3, stdout);
        //cpl_image_dump_window(img, 1, 1, 3, 3, stdout);
        cpl_test_eq(LEN(values) - 2, cpl_image_get(map, 2, 2, &dump));

        cpl_image_delete(map);
        cpl_imagelist_delete(list);
        cpl_imagelist_delete(dev);
    }

    {
        double * pixels = cpl_calloc(9, sizeof(double));
        cpl_msg_info(cpl_func, "-----------------");
        int values[] = { -3, 10, 10, 10, 10, 10, 10, 10 };
        int shifts[LEN(values) * 2] = {0};

        cpl_imagelist * list = cpl_imagelist_new();
        cpl_imagelist * dev = cpl_imagelist_new();

        cpl_image* origimg = cpl_image_wrap_double(3, 3, pixels);
        for (size_t i = 0; i < LEN(values); i++) {
            cpl_image * img = cpl_image_duplicate(origimg);
            if (i == 1) {
                cpl_image_set(img, 1, 2, values[i]);
                shifts[i * 2] = -1;
            }
            else if (i == 2) {
                cpl_image_set(img, 2, 3, values[i]);
                shifts[i * 2 + 1] = 1;
            }
            else if (i == 3) {
                cpl_image_set(img, 3, 1, values[i]);
                shifts[i * 2] = 1;
                shifts[i * 2 + 1] = -1;
            }
            else
                cpl_image_set(img, 2, 2, values[i]);
            cpl_imagelist_set(list, img, i);
        }
        cpl_image_delete(origimg);

        visir_util_clip_kappa_sigma_double(list, dev, 1.0, 2, 2, shifts);

        cpl_image * map = cpl_image_new_from_accepted(list);
        //cpl_image_dump_window(map, 1, 1, 3, 3, stdout);
        /* to test boundaries change recipes/visir_util_clip_body.c to mark
         * pixels shifted out of bounds as bad */
        if (0) {
            cpl_test_eq(LEN(values) - 2, cpl_image_get(map, 1, 1, &dump));
            cpl_test_eq(LEN(values) - 1, cpl_image_get(map, 1, 2, &dump));
            cpl_test_eq(LEN(values) - 2, cpl_image_get(map, 1, 3, &dump));
            cpl_test_eq(LEN(values) - 1, cpl_image_get(map, 2, 1, &dump));
            cpl_test_eq(LEN(values) - 1, cpl_image_get(map, 2, 2, &dump)); // one rejected
            cpl_test_eq(LEN(values) - 1, cpl_image_get(map, 2, 3, &dump));
            cpl_test_eq(LEN(values) - 1, cpl_image_get(map, 3, 1, &dump));
            cpl_test_eq(LEN(values) - 1, cpl_image_get(map, 3, 2, &dump));
            cpl_test_eq(LEN(values) - 2, cpl_image_get(map, 3, 3, &dump));
        }
        else
            cpl_test_eq(LEN(values) - 1, cpl_image_get(map, 2, 2, &dump));

        cpl_image_delete(map);
        cpl_imagelist_delete(list);
        cpl_imagelist_delete(dev);
    }

    /* test shifted rejects */
    {
        double * pixels = cpl_calloc(9, sizeof(double));
        /* gaus mean 100 sigma 5, 2 sigma range, 3 outliers */
        int values[] = {1, 150, -94, 94, 95, 95, 96, 96, 96, 97,
                        97, 97, 97, 98, 98, 98, 98, 99, 99, 99,
                        99, 100, 100, 100, 100, 100, 101, 101, 101, 101,
                        102, 102, 102, 102, 103, 103, 103, 103, 104, 104,
                        104, 105, 105, 106, 106, 107, 108 };
        int shifts[LEN(values) * 2] = {0};
        shifts[2] = 1;
        shifts[4] = 1;
        cpl_image * out1, * out2;

        cpl_imagelist * list = cpl_imagelist_new();
        cpl_imagelist * dev = cpl_imagelist_new();

        cpl_image* img = cpl_image_wrap_double(3, 3, pixels);
        for (size_t i = 0; i < LEN(values); i++) {
            cpl_image_set(img, 2, 2, values[i]);
            cpl_imagelist_set(list, img, i);
            img = cpl_image_duplicate(img);
        }
        cpl_image_delete(img);
        out1 = cpl_imagelist_get(list, 1);
        out2 = cpl_imagelist_get(list, 2);
        cpl_image_set(out1, 2, 2, 0);
        cpl_image_set(out1, 3, 2, -94);
        cpl_image_set(out2, 2, 2, 0);
        cpl_image_set(out2, 3, 2, 150);

        visir_util_clip_kappa_sigma_double(list, dev, 1.0, 3, 3, shifts);

        cpl_image * map = cpl_image_new_from_accepted(list);
        //cpl_image_dump_structure(map, stdout);
        //cpl_image_dump_window(map, 2, 2, 3, 3, stdout);
        //cpl_image_dump_window(img, 1, 1, 3, 3, stdout);
        cpl_test_eq(LEN(values) - 1, cpl_image_get(map, 2, 2, &dump));
        cpl_test_eq(LEN(values) - 2, cpl_image_get(map, 3, 2, &dump));
        dump = 0;
        cpl_image_get(out1, 3, 2, &dump);
        cpl_test_eq(dump, 1);
        dump = 0;
        cpl_image_get(out2, 3, 2, &dump);
        cpl_test_eq(dump, 1);

        cpl_image_delete(map);
        cpl_imagelist_delete(list);
        cpl_imagelist_delete(dev);
    }
    cpl_free(prnok);
    prnok_nz = 0;

    if (stream != stdout) cpl_test_zero( fclose(stream) );

}

static void visir_fftxcorrelate_test(void)
{
    FILE          * stream;

    stream = cpl_msg_get_level() > CPL_MSG_INFO
        ? fopen("/dev/null", "a") : stdout;

    cpl_test_nonnull( stream );
    {
        double xshift = 0, yshift = 0;
        double max_correlation;
        double * values = cpl_calloc(16, sizeof(double));
        cpl_error_code err;

        values[IND(2,2,4)] = 1;
        cpl_image * tmp = cpl_image_wrap_double(4, 4, values);
        values = cpl_calloc(16, sizeof(double));
        values[IND(3,3,4)] = 1;
        cpl_image * img2 = cpl_image_wrap_double(4, 4, values);

        err = visir_fftxcorrelate(tmp, img2, CPL_TRUE, &xshift, &yshift,
                                  &max_correlation, NULL);
        cpl_test_eq(CPL_ERROR_NONE, err);

        cpl_test_rel(1, xshift, 0.05);
        cpl_test_rel(1, yshift, 0.05);

        err = visir_fftxcorrelate(tmp, img2, CPL_TRUE,
                                  &xshift, NULL, NULL, NULL);
        cpl_test_eq(CPL_ERROR_NONE, err);
        cpl_test_rel(1, xshift, 0.05);

        err = visir_fftxcorrelate(tmp, img2, CPL_TRUE,
                                  NULL, &yshift, NULL, NULL);
        cpl_test_eq(CPL_ERROR_NONE, err);
        cpl_test_rel(1, yshift, 0.05);

        err = visir_fftxcorrelate(tmp, img2, CPL_TRUE, NULL, NULL,
                                  &max_correlation, NULL);
        cpl_test_eq(CPL_ERROR_NONE, err);
        cpl_test_lt(0.91, max_correlation);

        err = visir_fftxcorrelate(tmp, img2, CPL_FALSE,
                                  NULL, NULL, NULL, NULL);
        cpl_test_eq(CPL_ERROR_NONE, err);

        cpl_image_delete(tmp);
        cpl_image_delete(img2);
    }
    /* same image */
    {
        cpl_size N = 10;
        double xshift = 0, yshift = 0;
        double max_correlation;
        double * values = cpl_calloc(N*(N+5), sizeof(double));
        cpl_error_code err;

        values[IND(5,5,N)] = 1;
        values[IND(5,6,N)] = 9;
        values[IND(3,5,N)] = 2;
        values[IND(3,7,N)] = -3;
        cpl_image * tmp = cpl_image_wrap_double(N,N+5, values);
        cpl_image * img2 = cpl_image_duplicate(tmp);

        err = visir_fftxcorrelate(tmp, img2, CPL_TRUE, &xshift, &yshift,
                                  &max_correlation, NULL);
        cpl_test_eq(CPL_ERROR_NONE, err);

        cpl_test_rel(1, xshift + 1, 0.05);
        cpl_test_rel(1, yshift + 1, 0.05);
        cpl_test_rel(1.00, max_correlation, 0.02);

        cpl_image_delete(tmp);
        cpl_image_delete(img2);
    }
    /* subpixel shift test */
    {
        cpl_size N = 5;
        double xshift = 0, yshift = 0;
        double * values = cpl_calloc(N*N, sizeof(double));
        cpl_error_code err;

        values[IND(2,1,N)] = 1;
        values[IND(2,2,N)] = 3;
        values[IND(2,3,N)] = 2;
        cpl_image * tmp = cpl_image_wrap_double(N, N, values);
        values = cpl_calloc(N*N, sizeof(double));
        values[IND(2,1,N)] = 0.5;
        values[IND(2,2,N)] = 2;
        values[IND(2,3,N)] = 2.5;
        values[IND(2,4,N)] = 1;
        cpl_image * img2 = cpl_image_wrap_double(N, N, values);

        err = visir_fftxcorrelate(tmp, img2, CPL_TRUE, &xshift, &yshift,
                                  NULL, NULL);
        cpl_test_eq(CPL_ERROR_NONE, err);

        cpl_test_rel(1, xshift+1, 0.05);
        /* subpixel estimate has low precision */
        cpl_test_rel(0.5, yshift, 0.15);

        cpl_image_delete(tmp);
        cpl_image_delete(img2);
    }
    /* subpixel shift test 2*/
    {
        cpl_size N = 5;
        double xshift = 0, yshift = 0;
        double max_correlation;
        double * values = cpl_calloc(N*N, sizeof(double));
        cpl_error_code err;

        values[IND(2,1,N)] = 1;
        values[IND(2,2,N)] = 3;
        values[IND(2,3,N)] = 2;
        cpl_image * tmp = cpl_image_wrap_double(N, N, values);
        values = cpl_calloc(N*N, sizeof(double));
        values[IND(3,0,N)] = 2;
        values[IND(3,1,N)] = 2.5;
        values[IND(3,2,N)] = 1;
        cpl_image * img2 = cpl_image_wrap_double(N, N, values);

        err = visir_fftxcorrelate(tmp, img2, CPL_TRUE, &xshift, &yshift,
                                  &max_correlation, NULL);
        cpl_test_eq(CPL_ERROR_NONE, err);

        cpl_test_rel(1, xshift, 0.05);
        /* subpixel estimate has low precision */
        cpl_test_rel(-1.5, yshift, 0.1);

        cpl_image_delete(tmp);
        cpl_image_delete(img2);
    }
    /* test bad pixel */
    {
        double xshift = 0, yshift = 0;
        double max_correlation;
        double * values = cpl_calloc(16, sizeof(double));
        cpl_error_code err;

        values[IND(2,2,4)] = 1;
        cpl_image * tmp = cpl_image_wrap_double(4, 4, values);
        values = cpl_calloc(16, sizeof(double));
        values[IND(3,1,4)] = 2;
        values[IND(1,1,4)] = 200;
        cpl_image * img2 = cpl_image_wrap_double(4, 4, values);

        cpl_mask * m = cpl_mask_threshold_image_create(img2, 10, FLT_MAX);
        cpl_image_reject_from_mask(img2, m);
        cpl_mask_delete(m);

        err = visir_fftxcorrelate(tmp, img2, CPL_TRUE, &xshift, &yshift,
                                  &max_correlation, NULL);
        cpl_test_eq(CPL_ERROR_NONE, err);

        cpl_test_rel(1, xshift, 0.05);
        cpl_test_rel(-1, yshift, 0.05);

        cpl_image_delete(tmp);
        cpl_image_delete(img2);
    }
    /* test non-square */
    {
        cpl_size Nx = 8, Ny = 4;
        double xshift = 0, yshift = 0;
        double max_correlation;
        double * values = cpl_calloc(Nx*Ny, sizeof(double));
        cpl_error_code err;

        values[IND(2,1,Nx)] = 1;
        values[IND(2,2,Nx)] = 3;
        values[IND(2,3,Nx)] = 2;
        cpl_image * tmp = cpl_image_wrap_double(Nx, Ny, values);
        values = cpl_calloc(Nx*Ny, sizeof(double));
        values[IND(3,0,Nx)] = 2;
        values[IND(3,1,Nx)] = 2.5;
        values[IND(3,2,Nx)] = 1;
        cpl_image * img2 = cpl_image_wrap_double(Nx, Ny, values);

        err = visir_fftxcorrelate(tmp, img2, CPL_TRUE, &xshift, &yshift,
                                  &max_correlation, NULL);
        cpl_test_eq(CPL_ERROR_NONE, err);

        cpl_test_rel(1, xshift, 0.05);
        /* subpixel estimate has low precision */
        cpl_test_rel(-1.5, yshift, 0.1);

        cpl_image_delete(tmp);
        cpl_image_delete(img2);
    }
    /* test template reuse */
    {
        cpl_size Nx = 18, Ny = 27;
        double xshift = 0, yshift = 0;
        double max_correlation = 0;
        double * values = cpl_calloc(Nx*Ny, sizeof(double));
        cpl_error_code err;

        values[IND(5, 8,Nx)] = 1;
        values[IND(5, 9,Nx)] = 3;
        values[IND(5,10,Nx)] = 2;
        values[IND(4, 9,Nx)] = 1;
        values[IND(5, 9,Nx)] = 3;
        values[IND(6, 9,Nx)] = 2;
        cpl_image * tmp = cpl_image_wrap_double(Nx, Ny, values);
        values = cpl_calloc(Nx*Ny, sizeof(double));
        values[IND(4, 9,Nx)] = 1;
        values[IND(4,10,Nx)] = 3;
        values[IND(4,11,Nx)] = 2;
        values[IND(3,10,Nx)] = 1;
        values[IND(4,10,Nx)] = 3;
        values[IND(5,10,Nx)] = 2;
        cpl_image * img2 = cpl_image_wrap_double(Nx, Ny, values);
        visir_fftx_cache * cache = visir_new_fftx_cache();

        err = visir_fftxcorrelate(tmp, img2, CPL_TRUE, &xshift, &yshift,
                                  &max_correlation, cache);
        cpl_test_eq(CPL_ERROR_NONE, err);
        cpl_test_rel(-1, xshift, 0.05);
        cpl_test_rel(1, yshift, 0.05);
        cpl_test_rel(1, max_correlation, 0.05);

        double xshift2 = 0, yshift2 = 0;
        double max_correlation2 = 0;
        err = visir_fftxcorrelate(tmp, img2, CPL_TRUE, &xshift2, &yshift2,
                                  &max_correlation2, cache);
        cpl_test_eq(CPL_ERROR_NONE, err);
        cpl_test_eq(xshift, xshift2);
        cpl_test_eq(yshift, yshift2);
        cpl_test_eq(max_correlation, max_correlation2);

        cpl_image_delete(tmp);
        visir_delete_fftx_cache(cache);
        cpl_image_delete(img2);
    }

    if (stream != stdout) cpl_test_zero( fclose(stream) );
}

static void visir_interpolate_rejected_test(void)
{
    FILE          * stream;

    stream = cpl_msg_get_level() > CPL_MSG_INFO
        ? fopen("/dev/null", "a") : stdout;

    cpl_test_nonnull( stream );

    {
        float input[] = {
            1,9,3,9,
            0,9,4,1,
            9,2,0,1,
            1,0,3,0};
        cpl_binary bpmdata[] = {
            0,1,0,1,
            0,1,0,0,
            1,0,1,0,
            1,1,0,0};
        float expected[] = {
            1.0, 2.0, 3.0, 2.0,
            0.0, 2.0, 4.0, 1.0,
            1.0, 2.0, 2.5, 1.0,
            1.5, 2.5, 3.0, 0.0};


        cpl_image * in = cpl_image_wrap(4, 4, CPL_TYPE_FLOAT, input);
        cpl_mask * bpm = cpl_mask_wrap(4, 4, bpmdata);

        cpl_image_reject_from_mask(in, bpm);
        cpl_image * in2 = cpl_image_duplicate(in);
        cpl_image * in3 = cpl_image_duplicate(in);
        cpl_image * ex = cpl_image_wrap(4, 4, CPL_TYPE_FLOAT, expected);
        size_t *buffer = NULL;
        size_t n;
        visir_interpolate_rejected(in, &buffer, &n);
        cpl_test_error(CPL_ERROR_NONE);

        cpl_test_image_abs(in, ex, 1e-6);

        visir_interpolate_rejected(in2, &buffer, &n);
        cpl_test_error(CPL_ERROR_NONE);

        cpl_test_image_abs(in2, ex, 1e-6);
        cpl_test_image_abs(in2, in, 1e-6);

        visir_interpolate_rejected(in3, NULL, NULL);
        cpl_test_error(CPL_ERROR_NONE);

        cpl_test_image_abs(in3, ex, 1e-6);
        cpl_test_image_abs(in3, in, 1e-6);

        cpl_image_unwrap(in);
        cpl_image_delete(in2);
        cpl_image_delete(in3);
        cpl_mask_unwrap(bpm);
        cpl_image_unwrap(ex);
        cpl_free(buffer);
    }
    {
        float input[] = {
            1,7,3,9,9,9,
            0,9,4,1,1,1,
            9,2,0,2,2,2,
            1,0,3,3,0,2,
            1,1,3,0,1,0};
        cpl_binary bpmdata[] = {
            0,0,0,0,0,0,
            0,1,1,0,0,0,
            0,1,1,0,0,0,
            0,1,1,0,0,0,
            0,0,0,0,0,0};
        float expected[] = {
            1.0,7.0,3.0,9.0,9.0,9.0,
            0.0,8./3.,4./3.,1.0,1.0,1.0,
            9.0,4.75,4.25,2.0,2.0,2.0,
            1.0,5./3.,7./3.,3.0,0.0,2.0,
            1.0,1.0,3.0,0.0,1.0,0.0};


        cpl_image *  in = cpl_image_wrap(6, 5, CPL_TYPE_FLOAT, input);
        cpl_mask  *  bpm = cpl_mask_wrap(6, 5, bpmdata);
        cpl_image *  ex = cpl_image_wrap(6, 5, CPL_TYPE_FLOAT, expected);

        cpl_image_reject_from_mask(in, bpm);
        visir_interpolate_rejected(in, NULL, NULL);
        cpl_test_error(CPL_ERROR_NONE);

        cpl_test_image_abs(in, ex, 1e-6);

        cpl_image_unwrap(in);
        cpl_mask_unwrap(bpm);
        cpl_image_unwrap(ex);
    }
    {
        float input[] = {
            1,7,3,9,9,9,
            0,9,4,1,1,1,
            9,2,0,2,2,2,
            1,0,3,3,0,2,
            1,1,3,0,1,0};
        cpl_binary bpmdata[] = {
            0,0,0,0,0,0,
            0,1,1,1,1,0,
            0,1,1,1,1,1,
            0,0,0,0,0,0,
            1,1,1,1,1,1};
        float expected[] = {
            1.0,7.0,3.0,9.0,9.0,9.0,
            0.0,7./3.,2.0,13./3.,10./3.,1.0,
            9.0,16./3.,5.0,6.0,4.5,1.5,
            1.0,0.0,3.0,3.0,0.0,2.0,
            1.0,0.0,3.0,3.0,0.0,2.0};


        cpl_image *  in = cpl_image_wrap(6, 5, CPL_TYPE_FLOAT, input);
        cpl_mask  *  bpm = cpl_mask_wrap(6, 5, bpmdata);
        cpl_image *  ex = cpl_image_wrap(6, 5, CPL_TYPE_FLOAT, expected);

        cpl_image_reject_from_mask(in, bpm);
        cpl_image *  in2 = cpl_image_duplicate(in);
        size_t *buffer = NULL;
        size_t n;
        visir_interpolate_rejected(in, &buffer, &n);
        cpl_test_error(CPL_ERROR_NONE);

        cpl_test_image_abs(in, ex, 1e-6);

        visir_interpolate_rejected(in, &buffer, &n);
        cpl_test_error(CPL_ERROR_NONE);

        cpl_test_image_abs(in2, ex, 1e-6);
        cpl_test_image_abs(in2, in, 1e-6);

        cpl_image_unwrap(in);
        cpl_image_delete(in2);
        cpl_mask_unwrap(bpm);
        cpl_image_unwrap(ex);
        cpl_free(buffer);
    }

    if (stream != stdout) cpl_test_zero( fclose(stream) );
}

static void visir_mean_fast_test(void)
{
    FILE          * stream;

    stream = cpl_msg_get_level() > CPL_MSG_INFO
        ? fopen("/dev/null", "a") : stdout;

    cpl_test_nonnull( stream );

    {
        cpl_image * im = cpl_image_new(3, 43, CPL_TYPE_FLOAT);
        cpl_image_add_scalar(im, 3.2);
        cpl_test_abs(visir_image_get_mean_fast(im), 3.2, FLT_EPSILON);
        cpl_image_delete(im);
    }
    {
        cpl_image * im = cpl_image_new(4, 4, CPL_TYPE_FLOAT);
        cpl_image_add_scalar(im, 3.2);
        cpl_test_abs(visir_image_get_mean_fast(im), 3.2, FLT_EPSILON);
        cpl_image_delete(im);
    }
    {
        float val[] =
            {0.99226231, 0.6164352, 0.04541705, 0.94691027, 0.68742416};
        cpl_image * im = cpl_image_wrap(5, 1, CPL_TYPE_FLOAT, val);
        cpl_image_reject(im, 3, 1);
        cpl_test_abs(visir_image_get_mean_fast(im), 0.810758, FLT_EPSILON);
        cpl_image_unwrap(im);
    }
    {
        for (size_t i = 1; i < 17; i++) {
            cpl_image * im = cpl_image_new(1, i, CPL_TYPE_FLOAT);
            cpl_image_add_scalar(im, 3.2);
            cpl_test_abs(visir_image_get_mean_fast(im), 3.2, FLT_EPSILON);
            cpl_image_reject(im, 1, i);
            if (i == 1) {
                cpl_test_eq(visir_image_get_mean_fast(im), 0.);
            }
            else {
                cpl_test_abs(visir_image_get_mean_fast(im), 3.2, FLT_EPSILON);
            }
            cpl_image_delete(im);
        }
    }

    if (stream != stdout) cpl_test_zero( fclose(stream) );
}

static void visir_next_regular_test(void)
{
    FILE          * stream;

    stream = cpl_msg_get_level() > CPL_MSG_INFO
        ? fopen("/dev/null", "a") : stdout;

    cpl_test_nonnull( stream );

    cpl_test_eq(visir_get_next_regular(6), 6);
    cpl_test_eq(visir_get_next_regular(7), 8);
    cpl_test_eq(visir_get_next_regular(9), 9);
    cpl_test_eq(visir_get_next_regular(11), 12);
    cpl_test_eq(visir_get_next_regular(15), 15);
    cpl_test_eq(visir_get_next_regular(64), 64);
    cpl_test_eq(visir_get_next_regular(65), 72);
    cpl_test_eq(visir_get_next_regular(66), 72);
    cpl_test_eq(visir_get_next_regular(67), 72);
    cpl_test_eq(visir_get_next_regular(111), 120);
    cpl_test_eq(visir_get_next_regular(128), 128);
    cpl_test_eq(visir_get_next_regular(129), 135);
    cpl_test_eq(visir_get_next_regular(250), 250);
    cpl_test_eq(visir_get_next_regular(256), 256);
    cpl_test_eq(visir_get_next_regular(257), 270);
    cpl_test_eq(visir_get_next_regular(1023), 1024);
    cpl_test_eq(visir_get_next_regular(1024), 1024);
    cpl_test_eq(visir_get_next_regular(1025), 1080);
    cpl_test_eq(visir_get_next_regular(SIZE_MAX - 100), SIZE_MAX - 100);
    cpl_test_eq(visir_get_next_regular(SIZE_MAX), SIZE_MAX);

    for (cpl_size i = 0; i < 2048; i++) {
        size_t r = visir_get_next_regular(i);
        cpl_test(r >= (size_t)i);
        size_t d = 2;
        int large_primes = 0;
        while(r > 1) {
            while (r % d == 0) {
                large_primes += (d > 5);
                r /= d;
            }
            d++;
            if (d * d > r || large_primes) {
                large_primes += (r > 1) && (d > 5);
                break;
            }
        }
        /* avoid too many log messages by only testing after the loop */
        cpl_test_eq(large_primes, 0);
    }
    if (stream != stdout) cpl_test_zero( fclose(stream) );
}

static void visir_test_parallel_median(void)
{
    FILE          * stream;

    stream = cpl_msg_get_level() > CPL_MSG_INFO
        ? fopen("/dev/null", "a") : stdout;

    cpl_test_nonnull( stream );
    cpl_size nys[] = {1, 3, 4, 100, 105, 217};
    for (size_t j = 0; j < sizeof(nys)/sizeof(nys[0]); j++) {
        cpl_size ny = nys[j];
        cpl_imagelist * l = cpl_imagelist_new();
        size_t n = 57;
        cpl_size nx = 100;
        for (size_t i = 0; i < n; i++) {
            cpl_image * img = cpl_image_new(nx, ny, CPL_TYPE_FLOAT);
            float * d = cpl_image_get_data_float(img);
            for (size_t k = 0; k < (size_t)(nx * ny); k++) {
                d[i] = rand() % nx;
                /* fully bad timeline */
                cpl_image_reject(img, 1, 1);
                /* random bad pixels */
                if ((rand() % 25) == 0) {
                    cpl_image_reject(img, k % nx + 1, k / nx + 1);
                }
            }
            cpl_imagelist_set(l, img, i);
        }
        cpl_msg_info(cpl_func, "parallel median test ny: %ld", (long)ny);
        cpl_image * r1 = cpl_imagelist_collapse_median_create(l);
        cpl_image * r2 = visir_parallel_median_collapse(l);
        cpl_test_image_abs(r1, r2, 0);
        cpl_imagelist_delete(l);
        cpl_image_delete(r1);
        cpl_image_delete(r2);
    }
    cpl_test_eq_ptr(visir_parallel_median_collapse(NULL), NULL);
    cpl_test_error(CPL_ERROR_NULL_INPUT);

    cpl_imagelist * elist = cpl_imagelist_new();
    cpl_test_eq_ptr(visir_parallel_median_collapse(elist), NULL);
    cpl_test_error(CPL_ERROR_ILLEGAL_INPUT);
    cpl_imagelist_delete(elist);

    if (stream != stdout) cpl_test_zero( fclose(stream) );
}

static void visir_test_linintp_values(void)
{
    FILE          * stream;

    stream = cpl_msg_get_level() > CPL_MSG_INFO
        ? fopen("/dev/null", "a") : stdout;
    cpl_image * inp, * eimg, * res;
    double refx[] = {10, 20, 30};
    double refy[] = {1, 2, 1.5};
    size_t nref = sizeof(refx)/sizeof(refx[0]);
    cpl_vector * vrefx = cpl_vector_wrap(nref, refx);
    cpl_vector * vrefy = cpl_vector_wrap(nref, refy);
    cpl_bivector * ref = cpl_bivector_wrap_vectors(vrefx, vrefy);

    {
        double data[] = {10, 20, 30, 40, 15, 25, 12, 0, 5};
        //double edat[] = {1, 2, 1.5, 1, 1.5, 1.75, 1.2, 0, 0.5};
        double edat[] = {1, 2, 1.5, 1.5, 1.5, 1.75, 1.2, 1, 1.0};
        inp = cpl_image_wrap(2, 4, CPL_TYPE_DOUBLE, data);
        eimg = cpl_image_wrap(2, 4, CPL_TYPE_DOUBLE, edat);
        res = visir_linintp_values(inp, ref);
        cpl_test_image_abs(eimg, res, 1e-7);
        cpl_image_unwrap(inp);
        cpl_image_unwrap(eimg);
        cpl_image_delete(res);
    }
    cpl_bivector_unwrap_vectors(ref);
    cpl_vector_unwrap(vrefx);
    cpl_vector_unwrap(vrefy);

    if (stream != stdout) cpl_test_zero( fclose(stream) );
}
