/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set sw=2 sts=2 et cin: */
/*
 * This file is part of the MUSE Instrument Pipeline
 * Copyright (C) 2007-2014 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 Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#define _DEFAULT_SOURCE
#define _BSD_SOURCE /* get setenv() from stdlib.h */
#include <stdlib.h> /* setenv() */
#include <string.h>

#include <muse.h>

/*----------------------------------------------------------------------------*/
/**
  @brief    Test program to check that quadrant handling works.

  This program explicitly checks
    (muse_quadrants_overscan_stats, only minimally)
    muse_quadrants_overscan_polyfit_vertical
    muse_quadrants_overscan_get_window
    muse_quadrants_trim_image
    muse_quadrants_coords_to_raw
    muse_quadrants_get_window
 */
/*----------------------------------------------------------------------------*/
int main(int argc, char **argv)
{
  UNUSED_ARGUMENTS(argc, argv);
  cpl_test_init(PACKAGE_BUGREPORT, CPL_MSG_DEBUG);

  cpl_error_code rc;
  cpl_errorstate state = cpl_errorstate_get();
  /* create masks for median filtering */
  cpl_mask *hmask = cpl_mask_new(9, 1),
           *vmask = cpl_mask_new(1, 9);
  cpl_mask_not(hmask);
  cpl_mask_not(vmask);
  int i;
  for (i = 1; i <= kMuseNumIFUs; i++) {
    state = cpl_errorstate_get();
    /* load the file that contains a few normal and problematic channels */
    muse_image *sci = muse_image_load_from_raw(BASEFILENAME"_raw.fits.fz", i);
    if (!sci) { /* no more extensions */
      cpl_errorstate_set(state);
      break;
    }
    const char *extname = muse_pfits_get_extname(sci->header);
    /* Check overscan regions before (don't include prescans);      *
     * since we are only interested in large scale changes, median- *
     * filter them to reject cosmic rays and other rubbish.         */
    int nx = cpl_image_get_size_x(sci->data),
        ny = cpl_image_get_size_y(sci->data);
    cpl_image *hover = cpl_image_extract(sci->data, 33, 2089, nx-32, 2152),
              *vover = cpl_image_extract(sci->data, 2081, 33, 2144, ny-32),
              *hover2 = cpl_image_duplicate(hover),
              *vover2 = cpl_image_duplicate(vover);
    cpl_image_filter_mask(hover2, hover, hmask, CPL_FILTER_MEDIAN, CPL_BORDER_FILTER);
    cpl_image_filter_mask(vover2, vover, vmask, CPL_FILTER_MEDIAN, CPL_BORDER_FILTER);
    cpl_image_delete(hover);
    cpl_image_delete(vover);
    double mean1h = cpl_image_get_mean(hover2),
           stdev1h = cpl_image_get_stdev(hover2),
           mean1v = cpl_image_get_mean(vover2),
           stdev1v = cpl_image_get_stdev(vover2);
    cpl_image_delete(hover2);
    cpl_image_delete(vover2);
    if (i == 1) { /* the first time, try with debugging output on */
      setenv("MUSE_DEBUG_QUADRANTS", "1", 1);
    }
    state = cpl_errorstate_get();
    rc = muse_quadrants_overscan_polyfit_vertical(sci, 3, 3, 3., 1.01, 1.04);
    cpl_test(rc == CPL_ERROR_NONE && cpl_errorstate_is_equal(state));
    if (i == 1) {
      unsetenv("MUSE_DEBUG_QUADRANTS");
    }
    /* statistics again on filtered images */
    hover = cpl_image_extract(sci->data, 33, 2089, nx-32, 2152);
    vover = cpl_image_extract(sci->data, 2081, 33, 2144, ny-32);
    hover2 = cpl_image_duplicate(hover),
    vover2 = cpl_image_duplicate(vover);
    cpl_image_filter_mask(hover2, hover, hmask, CPL_FILTER_MEDIAN, CPL_BORDER_FILTER);
    cpl_image_filter_mask(vover2, vover, vmask, CPL_FILTER_MEDIAN, CPL_BORDER_FILTER);
    cpl_image_delete(hover);
    cpl_image_delete(vover);
    double mean2h = cpl_image_get_mean(hover2),
           stdev2h = cpl_image_get_stdev(hover2),
           mean2v = cpl_image_get_mean(vover2),
           stdev2v = cpl_image_get_stdev(vover2);
#if 0
    char *fnttt = cpl_sprintf("sci_test_hoverafter_%s.fits", extname);
    cpl_image_save(hover2, fnttt, CPL_TYPE_UNSPECIFIED, NULL, CPL_IO_CREATE);
    cpl_free(fnttt);
    fnttt = cpl_sprintf("sci_test_voverafter_%s.fits", extname);
    cpl_image_save(vover2, fnttt, CPL_TYPE_UNSPECIFIED, NULL, CPL_IO_CREATE);
    cpl_free(fnttt);
#endif
    cpl_image_delete(hover2);
    cpl_image_delete(vover2);
    cpl_msg_debug(__func__, "overscans before: %f +/- %f, %f +/- %f, after: "
                  "%f +/- %f, %f +/- %f", mean1h, stdev1h, mean1v, stdev1v,
                  mean2h, stdev2h, mean2v, stdev2v);
    /* now the mean values should be around zero, stdevs reduced to near RON */
    if (!strncmp("CHAN13", extname, 7) || !strncmp("CHAN22", extname, 7)) {
      cpl_test_abs(mean2h, 0., 0.2); // XXX no good results yet for these IFUs!
    } else {
      cpl_test_abs(mean2h, 0., 0.1);
    }
    cpl_test_abs(mean2v, 0., 0.05); /* the real result, here we should be strict */
    cpl_test(stdev2h < stdev1h && stdev2v < stdev1v);
    cpl_test(stdev2h < 1.0);
    cpl_test(stdev2v < 1.0);
#if 0
    char *fn = cpl_sprintf("sci_test_%s.fits", extname);
    muse_image_save(sci, fn);
    cpl_free(fn);
#endif
    muse_image_delete(sci);
  } /* for i */
  cpl_mask_delete(hmask);
  cpl_mask_delete(vmask);
  muse_image *raw = muse_image_load_from_raw(BASEFILENAME"_raw.fits.fz", 1);
  state = cpl_errorstate_get();
  rc = muse_quadrants_overscan_polyfit_vertical(NULL, 3, 3, 3., 1.01, 1.04);
  cpl_test(rc == CPL_ERROR_NULL_INPUT && !cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  rc = muse_quadrants_overscan_polyfit_vertical(raw, 3, 3, 3., 1., 1.04);
  cpl_test(rc == CPL_ERROR_ILLEGAL_INPUT && !cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  rc = muse_quadrants_overscan_polyfit_vertical(raw, 3, 3, 3., 1.01, 1.);
  cpl_test(rc == CPL_ERROR_ILLEGAL_INPUT && !cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  /* duplicate the raw image and mess with all relevant Q1 headers to test failures */
  muse_image *raw2 = muse_image_duplicate(raw);
  cpl_propertylist_erase(raw2->header, "ESO DET OUT2 NX");
  rc = muse_quadrants_overscan_polyfit_vertical(raw2, 3, 3, 3., 1.01, 1.04);
  cpl_test(rc == CPL_ERROR_BAD_FILE_FORMAT && !cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT1 NX", 0);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT2 NX", 0);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT3 NX", 0);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT4 NX", 0);
  rc = muse_quadrants_overscan_polyfit_vertical(raw2, 3, 3, 3., 1.01, 1.04);
  cpl_test(rc == CPL_ERROR_INCOMPATIBLE_INPUT && !cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT1 NX", 2048);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT2 NX", 2048);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT3 NX", 2048);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT4 NX", 2048);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT1 NY", 0);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT2 NY", 0);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT3 NY", 0);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT4 NY", 0);
  rc = muse_quadrants_overscan_polyfit_vertical(raw2, 3, 3, 3., 1.01, 1.04);
  cpl_test(rc == CPL_ERROR_INCOMPATIBLE_INPUT && !cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT1 NY", 2056);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT2 NY", 2056);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT3 NY", 2056);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT4 NY", 2056);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT1 PRSCX", 0);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT2 PRSCX", 0);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT3 PRSCX", 0);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT4 PRSCX", 0);
  rc = muse_quadrants_overscan_polyfit_vertical(raw2, 3, 3, 3., 1.01, 1.04);
  cpl_test(rc == CPL_ERROR_INCOMPATIBLE_INPUT && !cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT1 PRSCX", 32);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT2 PRSCX", 32);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT3 PRSCX", 32);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT4 PRSCX", 32);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT1 PRSCY", 0);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT2 PRSCY", 0);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT3 PRSCY", 0);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT4 PRSCY", 0);
  rc = muse_quadrants_overscan_polyfit_vertical(raw2, 3, 3, 3., 1.01, 1.04);
  cpl_test(rc == CPL_ERROR_INCOMPATIBLE_INPUT && !cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT1 PRSCY", 32);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT2 PRSCY", 32);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT3 PRSCY", 32);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT4 PRSCY", 32);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT1 OVSCY", 0);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT2 OVSCY", 0);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT3 OVSCY", 0);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT4 OVSCY", 0);
  rc = muse_quadrants_overscan_polyfit_vertical(raw2, 3, 3, 3., 1.01, 1.04);
  cpl_test(rc == CPL_ERROR_INCOMPATIBLE_INPUT && !cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  /* add again, smaller this time for the test below */
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT1 OVSCX", 2);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT2 OVSCX", 2);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT3 OVSCX", 2);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT4 OVSCX", 2);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT1 OVSCY", 0);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT2 OVSCY", 0);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT3 OVSCY", 0);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT4 OVSCY", 0);
  rc = muse_quadrants_overscan_polyfit_vertical(raw2, 3, 3, 3., 1.01, 1.04);
  cpl_test(rc == CPL_ERROR_INCOMPATIBLE_INPUT && !cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  /* too small OVSCX (compared to aIgnore) */
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT1 OVSCY", 32);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT2 OVSCY", 32);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT3 OVSCY", 32);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT4 OVSCY", 32);
  rc = muse_quadrants_overscan_polyfit_vertical(raw2, 3, 3, 3., 1.01, 1.04);
  cpl_test(rc == CPL_ERROR_ILLEGAL_INPUT && !cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  /* too large aIgnore (original file again) */
  rc = muse_quadrants_overscan_polyfit_vertical(raw, 32, 3, 3., 1.01, 1.04);
  cpl_test(rc == CPL_ERROR_ILLEGAL_INPUT && !cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  /* unexpected output port position */
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT2 X", 500);
  rc = muse_quadrants_overscan_polyfit_vertical(raw2, 1, 3, 3., 1.01, 1.04);
  cpl_test(rc == CPL_ERROR_ILLEGAL_INPUT && !cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  muse_image_delete(raw2);

  /* test muse_quadrants_overscan_get_window() */
  cpl_size *w;
  unsigned char nq;
  for (nq = 1; nq <= 4; nq++) {
    /* first set up the overscan windows for no ignored pixels */
    w = muse_quadrants_overscan_get_window(raw, nq, 0);
    /* check against values measured in ds9 on the raw image */
    switch (nq) {
    case 1:
      cpl_test_eq(w[0],   33); /* horizontal */
      cpl_test_eq(w[1], 2080);
      cpl_test_eq(w[2], 2089);
      cpl_test_eq(w[3], 2120);
      cpl_test_eq(w[4], 2081); /* vertical */
      cpl_test_eq(w[5], 2112);
      cpl_test_eq(w[6],   33);
      cpl_test_eq(w[7], 2120);
      break;
    case 2:
      cpl_test_eq(w[0], 2145); /* horizontal */
      cpl_test_eq(w[1], 4192);
      cpl_test_eq(w[2], 2089);
      cpl_test_eq(w[3], 2120);
      cpl_test_eq(w[4], 2113); /* vertical */
      cpl_test_eq(w[5], 2144);
      cpl_test_eq(w[6],   33);
      cpl_test_eq(w[7], 2120);
      break;
    case 3:
      cpl_test_eq(w[0], 2145); /* horizontal */
      cpl_test_eq(w[1], 4192);
      cpl_test_eq(w[2], 2121);
      cpl_test_eq(w[3], 2152);
      cpl_test_eq(w[4], 2113); /* vertical */
      cpl_test_eq(w[5], 2144);
      cpl_test_eq(w[6], 2121);
      cpl_test_eq(w[7], 4208);
      break;
    case 4:
      cpl_test_eq(w[0],   33); /* horizontal */
      cpl_test_eq(w[1], 2080);
      cpl_test_eq(w[2], 2121);
      cpl_test_eq(w[3], 2152);
      cpl_test_eq(w[4], 2081); /* vertical */
      cpl_test_eq(w[5], 2112);
      cpl_test_eq(w[6], 2121);
      cpl_test_eq(w[7], 4208);
    } /* switch */
    cpl_free(w);

    /* first set up the overscan windows for 5 ignored pixels, *
     * try with the environment variable this time             */
    setenv("MUSE_DEBUG_QUADRANTS", "1", 1);
    w = muse_quadrants_overscan_get_window(raw, nq, 5);
    /* again check against values measured in ds9 on the raw image */
    switch (nq) {
    case 1:
      cpl_test_eq(w[0],   33); /* horizontal */
      cpl_test_eq(w[1], 2080);
      cpl_test_eq(w[2], 2094);
      cpl_test_eq(w[3], 2120);
      cpl_test_eq(w[4], 2086); /* vertical */
      cpl_test_eq(w[5], 2112);
      cpl_test_eq(w[6],   33);
      cpl_test_eq(w[7], 2120);
      break;
    case 2:
      cpl_test_eq(w[0], 2145); /* horizontal */
      cpl_test_eq(w[1], 4192);
      cpl_test_eq(w[2], 2094);
      cpl_test_eq(w[3], 2120);
      cpl_test_eq(w[4], 2113); /* vertical */
      cpl_test_eq(w[5], 2139);
      cpl_test_eq(w[6],   33);
      cpl_test_eq(w[7], 2120);
      break;
    case 3:
      cpl_test_eq(w[0], 2145); /* horizontal */
      cpl_test_eq(w[1], 4192);
      cpl_test_eq(w[2], 2121);
      cpl_test_eq(w[3], 2147);
      cpl_test_eq(w[4], 2113); /* vertical */
      cpl_test_eq(w[5], 2139);
      cpl_test_eq(w[6], 2121);
      cpl_test_eq(w[7], 4208);
      break;
    case 4:
      cpl_test_eq(w[0],   33); /* horizontal */
      cpl_test_eq(w[1], 2080);
      cpl_test_eq(w[2], 2121);
      cpl_test_eq(w[3], 2147);
      cpl_test_eq(w[4], 2086); /* vertical */
      cpl_test_eq(w[5], 2112);
      cpl_test_eq(w[6], 2121);
      cpl_test_eq(w[7], 4208);
    } /* switch */
    cpl_free(w);
    unsetenv("MUSE_DEBUG_QUADRANTS");
  } /* for nq (all quadrants) */
  /* failure cases of muse_quadrants_overscan_get_window() */
  state = cpl_errorstate_get();
  w = muse_quadrants_overscan_get_window(NULL, 1, 5); /* NULL input */
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  cpl_test_null(w);
  w = muse_quadrants_overscan_get_window(raw, 0, 5); /* bad quad number */
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  cpl_test_null(w);
  w = muse_quadrants_overscan_get_window(raw, 5, 5); /* bad quad number */
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  cpl_test_null(w);
  w = muse_quadrants_overscan_get_window(raw, 1, 32); /* too large ignore */
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  cpl_test_null(w);
  /* for harder testcases mess with a duplicate header, always change one       *
   * property of one quadrant and then run muse_quadrants_overscan_get_window() *
   * on that quadrant to force the failure                                      */
  raw2 = muse_image_duplicate(raw);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT2 NX", -1);
  w = muse_quadrants_overscan_get_window(raw2, 2, 5);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(state);
  cpl_test_null(w);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT2 NX", 2048);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT3 NY", -1);
  w = muse_quadrants_overscan_get_window(raw2, 3, 5);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(state);
  cpl_test_null(w);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT3 NY", 2056);
  cpl_propertylist_erase(raw2->header, "ESO DET OUT1 PRSCX");
  w = muse_quadrants_overscan_get_window(raw2, 1, 5);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(state);
  cpl_test_null(w);
  cpl_propertylist_append_int(raw2->header, "ESO DET OUT1 PRSCX", 32);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT1 PRSCY", -1);
  w = muse_quadrants_overscan_get_window(raw2, 1, 5);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(state);
  cpl_test_null(w);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT1 PRSCY", 32);
  cpl_propertylist_erase(raw2->header, "ESO DET OUT4 OVSCX");
  w = muse_quadrants_overscan_get_window(raw2, 4, 5);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(state);
  cpl_test_null(w);
  cpl_propertylist_append_int(raw2->header, "ESO DET OUT4 OVSCX", 32);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT4 OVSCY", 0);
  w = muse_quadrants_overscan_get_window(raw2, 4, 5);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(state);
  cpl_test_null(w);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT4 OVSCY", 32);
  cpl_propertylist_erase(raw2->header, "ESO DET BINX");
  w = muse_quadrants_overscan_get_window(raw2, 1, 5);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(state);
  cpl_test_null(w);
  cpl_propertylist_append_int(raw2->header, "ESO DET BINX", 1);
  cpl_propertylist_erase(raw2->header, "ESO DET BINY");
  w = muse_quadrants_overscan_get_window(raw2, 3, 5);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(state);
  cpl_test_null(w);
  cpl_propertylist_append_int(raw2->header, "ESO DET BINX", 1);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT1 X", 10);
  w = muse_quadrants_overscan_get_window(raw2, 1, 5);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(state);
  cpl_test_null(w);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT1 X", 1);
  cpl_propertylist_update_int(raw2->header, "ESO DET OUT3 Y", 2000);
  w = muse_quadrants_overscan_get_window(raw2, 3, 5);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(state);
  cpl_test_null(w);
  muse_image_delete(raw2);

  /* do some (very basic!) testing of muse_quadrants_overscan_stats() */
  state = cpl_errorstate_get();
  rc = muse_quadrants_overscan_stats(raw, "dcr", 0);
  cpl_test(rc == CPL_ERROR_NONE && cpl_errorstate_is_equal(state));
  /* check the output FITS headers */
  for (nq = 1; nq <= 4; nq++) {
    char *kw = cpl_sprintf(MUSE_HDR_OVSC_MEAN, nq),
         *kwstd = cpl_sprintf(MUSE_HDR_OVSC_STDEV, nq);
    double mean = cpl_propertylist_get_double(raw->header, kw),
           stdev = cpl_propertylist_get_double(raw->header, kwstd);
    cpl_msg_debug(__func__, "nq = %hhu: %f +/- %f", nq, mean, stdev);
    /* all quadrants have overscans of about 1200 +/- 2 */
    cpl_test_abs(mean, 1200., 1.5);
    cpl_test_abs(stdev, 2., 0.25);
    cpl_free(kw);
    cpl_free(kwstd);
  } /* for nq (all quadrants) */
  muse_image_delete(raw);

  raw = muse_image_new();
  raw->data = cpl_image_load(BASEFILENAME"_trim.fits", CPL_TYPE_FLOAT, 0, 1);
  /* read the primary and extension header */
  raw->header = cpl_propertylist_load(BASEFILENAME"_trim.fits", 0);
  cpl_propertylist *extheader = cpl_propertylist_load(BASEFILENAME"_trim.fits", 1);
  /* append the extension header to the one in the muse_image */
  cpl_propertylist_append(raw->header, extheader);
  cpl_propertylist_delete(extheader);
  if (!raw->data || !raw->header) {
    muse_image_delete(raw);
    return cpl_test_end(1);
  }
  /* the other extensions shouldn't be needed for this test */
  raw->dq = NULL;
  raw->stat = NULL;
  double cputime = cpl_test_get_cputime();
  /* call the actual function to test */
  muse_image *trimmed = muse_quadrants_trim_image(raw);
  double diff = cpl_test_get_cputime() - cputime;
  cpl_test_nonnull(trimmed);
  if (!trimmed) {
    return cpl_test_end(1);
  }
  cpl_msg_info(__func__, "image trimmed using muse_quadrants_trim_image(), "
               "took %gs", diff);
  /* the trimmed image should be smaller than the input image */
  cpl_test(cpl_image_get_size_x(trimmed->data) < cpl_image_get_size_x(raw->data));
  cpl_test(cpl_image_get_size_y(trimmed->data) < cpl_image_get_size_y(raw->data));
  /* the trimmed image should be of normal MUSE size */
  cpl_test(cpl_image_get_size_x(trimmed->data) == kMuseOutputXRight);
  cpl_test(cpl_image_get_size_y(trimmed->data) == kMuseOutputYTop);
#if 0
  muse_image_save(trimmed, "muse_test_quadrants_trimmed.fits");
#endif
  muse_image_delete(trimmed);
  /* test trimming failures, too */
  state = cpl_errorstate_get();
  trimmed = muse_quadrants_trim_image(NULL); /* totally invalid MUSE image */
  cpl_test_null(trimmed);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  cpl_image *data = raw->data;
  raw->data = NULL;
  trimmed = muse_quadrants_trim_image(raw); /* missing ->data */
  cpl_test_null(trimmed);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  raw->data = data;
  cpl_propertylist *header = raw->header;
  raw->header = NULL;
  trimmed = muse_quadrants_trim_image(raw); /* missing ->header */
  cpl_test_null(trimmed);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  raw->header = cpl_propertylist_duplicate(header);
  cpl_propertylist_erase_regexp(raw->header, "DET OUT2", 0);
  trimmed = muse_quadrants_trim_image(raw); /* missing OUT2 keywords */
  cpl_test_null(trimmed);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_DATA_NOT_FOUND);
  cpl_errorstate_set(state);
  cpl_propertylist_delete(raw->header);
  raw->header = cpl_propertylist_duplicate(header);
  cpl_propertylist_erase_regexp(raw->header, "DET OUT[1-3] N(X|Y)", 0);
  trimmed = muse_quadrants_trim_image(raw); /* some missing NX/NY keywords */
  cpl_test_null(trimmed);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_DATA_NOT_FOUND);
  cpl_errorstate_set(state);
  cpl_propertylist_delete(raw->header);
  raw->header = cpl_propertylist_duplicate(header);
  cpl_propertylist_erase_regexp(raw->header, "DET OUT[34] (X|Y)", 0);
  trimmed = muse_quadrants_trim_image(raw); /* some missing output port keywords */
  cpl_test_null(trimmed);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_DATA_NOT_FOUND);
  cpl_errorstate_set(state);
  cpl_propertylist_delete(raw->header);
  raw->header = cpl_propertylist_duplicate(header);
  cpl_propertylist_update_int(raw->header, "ESO DET OUT4 X", 4000);
  cpl_propertylist_update_int(raw->header, "ESO DET OUT4 Y", 4100);
  trimmed = muse_quadrants_trim_image(raw); /* bad port 4 position */
  cpl_test_null(trimmed);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(state);
  cpl_propertylist_delete(raw->header);
  raw->header = cpl_propertylist_duplicate(header);
  cpl_propertylist_update_int(raw->header, "ESO DET OUT1 NX", 3000);
  cpl_propertylist_update_int(raw->header, "ESO DET OUT1 NY", 3000);
  trimmed = muse_quadrants_trim_image(raw); /* bad port 1 size (too large output image) */
  cpl_test_null(trimmed);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  cpl_propertylist_delete(raw->header);
  raw->header = cpl_propertylist_duplicate(header);
  cpl_propertylist_update_int(raw->header, "ESO DET OUT4 NX", 3000);
  cpl_propertylist_update_int(raw->header, "ESO DET OUT4 NY", 3000);
  trimmed = muse_quadrants_trim_image(raw); /* bad port 4 size (incompatible with other ports) */
  cpl_test_null(trimmed);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(state);
  cpl_propertylist_delete(raw->header);
  raw->header = cpl_propertylist_duplicate(header);
  cpl_propertylist_update_int(raw->header, "ESO DET OUT1 NX", 10);
  cpl_propertylist_update_int(raw->header, "ESO DET OUT1 NY", 10);
  cpl_propertylist_update_int(raw->header, "ESO DET OUT2 NX", 10);
  cpl_propertylist_update_int(raw->header, "ESO DET OUT2 NY", 10);
  cpl_propertylist_update_int(raw->header, "ESO DET OUT3 NX", 10);
  cpl_propertylist_update_int(raw->header, "ESO DET OUT3 NY", 10);
  cpl_propertylist_update_int(raw->header, "ESO DET OUT4 NX", 10);
  cpl_propertylist_update_int(raw->header, "ESO DET OUT4 NY", 10);
  trimmed = muse_quadrants_trim_image(raw); /* all ports small (should work!) */
  cpl_test_nonnull(trimmed);
  muse_image_delete(trimmed);
  cpl_test(cpl_errorstate_is_equal(state));
  state = cpl_errorstate_get();
  cpl_propertylist_update_int(raw->header, "ESO DET OUT1 NX", 3000);
  cpl_propertylist_update_int(raw->header, "ESO DET OUT1 NY", 3000);
  cpl_propertylist_update_int(raw->header, "ESO DET OUT2 NX", 3000);
  cpl_propertylist_update_int(raw->header, "ESO DET OUT2 NY", 3000);
  cpl_propertylist_update_int(raw->header, "ESO DET OUT3 NX", 3000);
  cpl_propertylist_update_int(raw->header, "ESO DET OUT3 NY", 3000);
  cpl_propertylist_update_int(raw->header, "ESO DET OUT4 NX", 3000);
  cpl_propertylist_update_int(raw->header, "ESO DET OUT4 NY", 3000);
  trimmed = muse_quadrants_trim_image(raw); /* all ports very large (should fail!) */
  cpl_test_null(trimmed);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  cpl_propertylist_delete(raw->header);
  raw->header = header;

  /* check simple conversion from trimmed to raw coordinates */
  state = cpl_errorstate_get();
  rc = muse_quadrants_coords_to_raw(NULL, NULL, NULL);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  int xcoords = 1, ycoords = 1;
  rc = muse_quadrants_coords_to_raw(NULL, &xcoords, NULL);
  cpl_test(cpl_errorstate_is_equal(state));
  cpl_test(xcoords == 33);
  rc = muse_quadrants_coords_to_raw(NULL, NULL, &ycoords);
  cpl_test(cpl_errorstate_is_equal(state));
  cpl_test(ycoords == 33);
  xcoords = 4000, ycoords = 4000;
  rc = muse_quadrants_coords_to_raw(NULL, &xcoords, &ycoords);
  cpl_test(cpl_errorstate_is_equal(state));
  cpl_test(xcoords == 4000 + 3*32);
  cpl_test(ycoords == 4000 + 3*32);
  /* specific test cases for a real case that was wrong */
  xcoords = 2024, ycoords = 2049;
  rc = muse_quadrants_coords_to_raw(NULL, &xcoords, &ycoords);
  cpl_test_eq(xcoords, 2056);
  cpl_test_eq(ycoords, 2081);
  /* specific test cases for the corners around the central overscan */
  xcoords = 2048, ycoords = 2056;
  rc = muse_quadrants_coords_to_raw(NULL, &xcoords, &ycoords);
  cpl_test_eq(xcoords, 2080);
  cpl_test_eq(ycoords, 2088);
  xcoords = 2048, ycoords = 2057;
  rc = muse_quadrants_coords_to_raw(NULL, &xcoords, &ycoords);
  cpl_test_eq(xcoords, 2080);
  cpl_test_eq(ycoords, 2153);
  xcoords = 2049, ycoords = 2057;
  rc = muse_quadrants_coords_to_raw(NULL, &xcoords, &ycoords);
  cpl_test_eq(xcoords, 2145);
  cpl_test_eq(ycoords, 2153);
  xcoords = 2049, ycoords = 2056;
  rc = muse_quadrants_coords_to_raw(NULL, &xcoords, &ycoords);
  cpl_test_eq(xcoords, 2145);
  cpl_test_eq(ycoords, 2088);

  /* test quadrant window positions */
  /* test all quadrants in raw and trimmed image */
  state = cpl_errorstate_get();
  cpl_size *wraw = muse_quadrants_get_window(raw, 1); /* bottom left */
  cpl_test(wraw[0] == 32+1 && wraw[1] == 32+2048 &&
           wraw[2] == 32+1 && wraw[3] == 32+2056);
  cpl_free(wraw);
  wraw = muse_quadrants_get_window(raw, 2); /* bottom right */
  cpl_test(wraw[0] == 3*32+2048+1 && wraw[1] == 3*32+2*2048 &&
           wraw[2] == 32+1 && wraw[3] == 32+2056);
  cpl_free(wraw);
  wraw = muse_quadrants_get_window(raw, 3); /* top right */
  cpl_test(wraw[0] == 3*32+2048+1 && wraw[1] == 3*32+2*2048 &&
           wraw[2] == 3*32+2056+1 && wraw[3] == 3*32+2*2056);
  cpl_free(wraw);
  wraw = muse_quadrants_get_window(raw, 4); /* top left */
  cpl_test(wraw[0] == 32+1 && wraw[1] == 32+2048 &&
           wraw[2] == 3*32+2056+1 && wraw[3] == 3*32+2*2056);
  cpl_free(wraw);
  cpl_test(cpl_errorstate_is_equal(state)); /* no errors for the raw windows */
  trimmed = muse_quadrants_trim_image(raw); /* trim again */
  state = cpl_errorstate_get();
  cpl_size *wtr = muse_quadrants_get_window(trimmed, 1); /* bottom left */
  cpl_test(wtr[0] == 1 && wtr[1] == 2048 &&
           wtr[2] == 1 && wtr[3] == 2056);
  cpl_free(wtr);
  wtr = muse_quadrants_get_window(trimmed, 2); /* bottom right */
  cpl_test(wtr[0] == 1+2048 && wtr[1] == 2*2048 &&
           wtr[2] == 1 && wtr[3] == 2056);
  cpl_free(wtr);
  wtr = muse_quadrants_get_window(trimmed, 3); /* top right */
  cpl_test(wtr[0] == 1+2048 && wtr[1] == 2*2048 &&
           wtr[2] == 1+2056 && wtr[3] == 2*2056);
  cpl_free(wtr);
  wtr = muse_quadrants_get_window(trimmed, 4); /* top left */
  cpl_test(wtr[0] == 1 && wtr[1] == 2048 &&
           wtr[2] == 1+2056 && wtr[3] == 2*2056);
  cpl_free(wtr);
  cpl_test(cpl_errorstate_is_equal(state)); /* no errors for the trimmed windows */
  /* try the same again, now with an assymetrically cut image and adapted headers */
  muse_image *x = muse_image_new();
  x->data = cpl_image_extract(trimmed->data, 1780, 1922, 3000, 4106);
  x->header = cpl_propertylist_duplicate(trimmed->header);
  cpl_propertylist_update_int(x->header, "ESO DET OUT1 NX", 269); /* [1780:2048,1922:2056] */
  cpl_propertylist_update_int(x->header, "ESO DET OUT1 NY", 135);
  cpl_propertylist_update_int(x->header, "ESO DET OUT2 NX", 952); /* [2049:3000,1922:2056] */
  cpl_propertylist_update_int(x->header, "ESO DET OUT2 NY", 135);
  cpl_propertylist_update_int(x->header, "ESO DET OUT3 NX", 269); /* [1780:2048,2057:4106] */
  cpl_propertylist_update_int(x->header, "ESO DET OUT3 NY", 2050);
  cpl_propertylist_update_int(x->header, "ESO DET OUT4 NX", 952); /* [2049:3000,2057:4106] */
  cpl_propertylist_update_int(x->header, "ESO DET OUT4 NY", 2050);
  state = cpl_errorstate_get();
  cpl_size *wx = muse_quadrants_get_window(x, 1); /* bottom left */
  cpl_test(wx[0] == 1 && wx[1] == 269 &&
           wx[2] == 1 && wx[3] == 135);
  cpl_free(wx);
  wx = muse_quadrants_get_window(x, 2); /* bottom right */
  cpl_test(wx[0] == 1+269 && wx[1] == 269+952 &&
           wx[2] == 1 && wx[3] == 135);
  cpl_free(wx);
  wx = muse_quadrants_get_window(x, 3); /* top right */
  cpl_test(wx[0] == 1+269 && wx[1] == 269+952 &&
           wx[2] == 1+135 && wx[3] == 135+2050);
  cpl_free(wx);
  wx = muse_quadrants_get_window(x, 4); /* top left */
  cpl_test(wx[0] == 1 && wx[1] == 269 &&
           wx[2] == 1+135 && wx[3] == 135+2050);
  cpl_free(wx);
  cpl_test(cpl_errorstate_is_equal(state)); /* no errors for the x-trimmed windows */
  muse_image_delete(x);
  muse_image_delete(trimmed);
  /* test some failures */
  state = cpl_errorstate_get();
  wx = muse_quadrants_get_window(NULL, 2); /* NULL image */
  cpl_test_null(wx);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  wx = muse_quadrants_get_window(x, 2); /* x is valid but its components are NULL */
  cpl_test_null(wx);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  wx = muse_quadrants_get_window(raw, 0); /* too low quadrant number */
  cpl_test_null(wx);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  wx = muse_quadrants_get_window(raw, 5); /* too high quadrant number */
  cpl_test_null(wx);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  muse_image_delete(raw);

  return cpl_test_end(0);
}
