/* -*- 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) 2005-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.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

/*----------------------------------------------------------------------------*
 *                             Includes                                       *
 *----------------------------------------------------------------------------*/
#include <cpl.h>
#include <string.h> /* strncmp() */

#include "muse_quadrants.h"
#include "muse_instrument.h"

#include "muse_artifacts.h"
#include "muse_dfs.h"
#include "muse_pfits.h"
#include "muse_utils.h"

/*----------------------------------------------------------------------------*/
/**
 * @defgroup muse_quadrants    Quadrants and other CCD regions
 *
 * This group of functions handles regions on the CCDs, like quadrants, pre- and
 * overscans, trimming of the pre- and overscans, and overscan statistics.
 */
/*----------------------------------------------------------------------------*/

/**@{*/

/*---------------------------------------------------------------------------*/
/**
  @brief   Compute overscan statistics of all quadrants and save in FITS header.
  @param   aImage       muse_image that holds the image and header to work on
  @param   aRejection   rejection method (and possibly parameters)
  @param   aIgnore      number of pixels to ignore at the inner edge of the
                        overscans
  @return  CPL_ERROR_NONE on success or a cpl_error_code on error
  @note    This function assumes that the sizes of all quadrants and their pre-
           and overscans are identical. It ensures this by calling
           muse_quadrants_verify().

  Recognized aRejection methods are "none", "dcr" and "fit". aRejection may
  also contain more parameters for the respective type. Parameters to "none"
  are ignored. For "dcr" three unsigned integers and one float can be passed,
  like "dcr:128,32,3,2.5" (two box sizes, the number of passes, and the sigma
  threshold). For "fit" one float can be passed, like "fit:4.0" (the iterative
  rejection sigma).

  @error{return CPL_ERROR_NULL_INPUT,
         if aImage or its data or header component are NULL}
  @error{return CPL_ERROR_BAD_FILE_FORMAT,
         region sizes are not equal for all quadrants}
  @error{return CPL_ERROR_ILLEGAL_INPUT,
         FITS headers are missing required keywords or aIgnore as large as the horizontal overscan width or larger}
 */
/*---------------------------------------------------------------------------*/
cpl_error_code
muse_quadrants_overscan_stats(muse_image *aImage, const char *aRejection,
                              unsigned int aIgnore)
{
  cpl_ensure_code(aImage && aImage->data && aImage->header, CPL_ERROR_NULL_INPUT);
  /* ensure that the region sizes are the same for all quadrants */
  cpl_ensure_code(muse_quadrants_verify(aImage->header),
                  CPL_ERROR_BAD_FILE_FORMAT);
  /* Check that the overscan windows make sense for all quadrants; *
   * do this early to not waste time on other computations before. *
   * This implicitly checks, if the input aIgnore value is usable. */
  unsigned char n; /* quadrant counter */
  for (n = 1; n <= 4; n++) {
    cpl_size *w = muse_quadrants_overscan_get_window(aImage, n, aIgnore);
    cpl_ensure_code(w, CPL_ERROR_ILLEGAL_INPUT);
    cpl_free(w);
  } /* for n (quadrants) */
  int nx = cpl_image_get_size_x(aImage->data);

  enum { REJ_NONE, REJ_DCR, REJ_FIT } reject = REJ_NONE;
  if (aRejection) {
    if (!strncmp(aRejection, "dcr", 3)) { /* allow parameters after string! */
      reject = REJ_DCR;
    } else if (!strncmp(aRejection, "fit", 3)) { /* allow parameters after string! */
      reject = REJ_FIT;
    } else if (strncmp(aRejection, "none", 5)) {
      return cpl_error_set_message(__func__, CPL_ERROR_ILLEGAL_INPUT,
                                   "Unknown rejection type \"%s\"", aRejection);
    }
  } /* if aRejection */

  unsigned int binx = muse_pfits_get_binx(aImage->header),
               biny = muse_pfits_get_biny(aImage->header),
               outnx = muse_pfits_get_out_nx(aImage->header, 1) / binx,
               outny = muse_pfits_get_out_ny(aImage->header, 1) / biny,
               overx = muse_pfits_get_out_overscan_x(aImage->header, 1) / binx,
               overy = muse_pfits_get_out_overscan_y(aImage->header, 1) / biny;
  unsigned long dcrbox1 = 128, dcrbox2 = 32, dcrnpass = 3;
  float dcrthres = 2.5,
        sigma = 5.0;
  if (reject == REJ_DCR) {
    char *rest = strchr(aRejection, ':');
    if (strlen(aRejection) > 4 && rest++) { /* try to access info after "dcr:" */
      dcrbox1 = strtoul(rest, &rest, 10);
      if (strlen(rest++) > 0) { /* ++ to skip over the comma */
        dcrbox2 = strtoul(rest, &rest, 10);
        if (strlen(rest++) > 0) {
          dcrnpass = strtoul(rest, &rest, 10);
          if (strlen(rest++) > 0) {
            dcrthres = strtof(rest, NULL);
          }
        }
      }
    }
    /* check that box sizes in both directions stay inside overscan regions */
    if (dcrbox2 > overx || dcrbox2 > overy ||
        dcrbox1 > outnx || dcrbox1 > outny ||
        dcrnpass <= 0 || dcrthres <= 0.) {
      cpl_msg_warning(__func__, "Detected illegal DCR parameters for overscan "
                      "statistics (%lux%lu, %lu, %f; overscan: %ux%u, output: "
                      "%ux%u), not rejecting anything!", dcrbox1, dcrbox2,
                      dcrnpass, dcrthres, overx, overy, outnx, outny);
      reject = REJ_NONE;
    } else {
      cpl_msg_debug(__func__, "Using DCR cosmic ray rejection for overscan "
                    "statistics (%lux%lu, %lu, %f)", dcrbox1, dcrbox2, dcrnpass,
                    dcrthres);
    }
  } else if (reject == REJ_FIT) {
    char *rest = strchr(aRejection, ':');
    if (strlen(aRejection) > 4 && rest++) { /* try to access info after "fit:" */
      sigma = strtof(rest, NULL);
    }
    /* check that box sizes in both directions stay inside overscan regions */
    if (sigma <= 0.) {
      cpl_msg_warning(__func__, "Detected illegal fit sigma for overscan "
                      "statistics (%f; overscan: %ux%u, output: %ux%u) not "
                      "rejecting anything!", sigma, overx, overy, outnx, outny);
      reject = REJ_NONE;
    } else {
      cpl_msg_debug(__func__, "Using constant fit for overscan statistics (0, "
                    "%f)", sigma);
    }
  } /* if REJ_DCR */

  const float *data = cpl_image_get_data_float_const(aImage->data);
  const int *dq = cpl_image_get_data_int_const(aImage->dq);

  /* get data region for each quadrant and compute the size of the output image */
  for (n = 1; n <= 4; n++) {
    /* get overscan regions and compute pixel numbers */
    cpl_size *w = muse_quadrants_overscan_get_window(aImage, n, aIgnore);
    int nh1 = w[1] - w[0] + 1,
        nh2 = w[3] - w[2] + 1,
        nhori = nh1 * nh2,
        nv1 = w[5] - w[4] + 1,
        nv2 = w[7] - w[6] + 1,
        nvert = nv1 * nv2,
        ntot = nhori + nvert,
        nhcr = 0, nvcr = 0;

    if (reject == REJ_DCR && dcrnpass > 0 && dcrbox1 > 0 && dcrbox2 > 0) {
      /* extract subframes to use for cosmic ray marking, then put them back */
      /* horizontal part */
      muse_image *image = muse_image_new();
      image->data = cpl_image_extract(aImage->data, w[0], w[2], w[1], w[3]);
      image->dq = cpl_image_extract(aImage->dq, w[0], w[2], w[1], w[3]);
      image->stat = cpl_image_extract(aImage->stat, w[0], w[2], w[1], w[3]);
      /* make sure to not cause illegal inputs due to too large boxes */
      nhcr = muse_cosmics_dcr(image, CPL_MIN((int)dcrbox1, nh1),
                              CPL_MIN((int)dcrbox2, nh2),
                              dcrnpass, dcrthres);
      cpl_image_copy(aImage->data, image->data, w[0], w[2]);
      cpl_image_copy(aImage->dq, image->dq, w[0], w[2]);
      cpl_image_copy(aImage->stat, image->stat, w[0], w[2]);
      muse_image_delete(image);
      /* vertical part */
      image = muse_image_new();
      image->data = cpl_image_extract(aImage->data, w[4], w[6], w[5], w[7]);
      image->dq = cpl_image_extract(aImage->dq, w[4], w[6], w[5], w[7]);
      image->stat = cpl_image_extract(aImage->stat, w[4], w[6], w[5], w[7]);
      /* inverted box size */
      nvcr = muse_cosmics_dcr(image, CPL_MIN((int)dcrbox2, nv1),
                              CPL_MIN((int)dcrbox1, nv2),
                              dcrnpass, dcrthres);
      cpl_image_copy(aImage->data, image->data, w[4], w[6]);
      cpl_image_copy(aImage->dq, image->dq, w[4], w[6]);
      cpl_image_copy(aImage->stat, image->stat, w[4], w[6]);
      muse_image_delete(image);
    } /* if REJ_DCR */

    /* now loop over both regions and fill vectors for the statistics */
    cpl_vector *hori = cpl_vector_new(nhori),
               *vert = cpl_vector_new(nvert),
               *tot = cpl_vector_new(ntot);
    /* collect all pixels in the horizontal overscan */
    int i, j, ihori = 0, ivert = 0;
    for (i = w[0] - 1; i < w[1]; i++) {
      for (j = w[2] - 1; j < w[3]; j++) {
        if (dq[i + j*nx]) { /* exclude pixels with bad status */
          continue;
        }
        cpl_vector_set(tot, ihori + ivert, data[i + j*nx]);
        cpl_vector_set(hori, ihori++, data[i + j*nx]);
      }
    }
    for (i = w[4] - 1; i < w[5]; i++) {
      for (j = w[6] - 1; j < w[7]; j++) {
        if (dq[i + j*nx]) {
          continue;
        }
        cpl_vector_set(tot, ihori + ivert, data[i + j*nx]);
        cpl_vector_set(vert, ivert++, data[i + j*nx]);
      }
    }
    cpl_vector_set_size(hori, ihori);
    cpl_vector_set_size(vert, ivert);
    cpl_vector_set_size(tot, ihori + ivert);

    int nrej = 0;
    double mean, stdev;
    if (reject == REJ_FIT) {
      /* For the iterative fit, we only care about the mean, not    *
       * about the position, so leave the matrix filled with zeros. */
      cpl_matrix *pos = cpl_matrix_new(1, ihori + ivert);
      cpl_vector *val = cpl_vector_duplicate(tot);
      double mse;
      cpl_polynomial *fit = muse_utils_iterate_fit_polynomial(pos, val, NULL, NULL,
                                                              0, sigma, &mse, NULL);
      nrej = ihori + ivert - cpl_vector_get_size(val);
      cpl_size pows = 0; /* get the zero-order coefficient */
      mean = cpl_polynomial_get_coeff(fit, &pows);
      stdev = sqrt(mse);
      cpl_polynomial_delete(fit);
      cpl_matrix_delete(pos);
      cpl_vector_delete(val);
    } else { /* no fitting */
      nrej = nhcr + nvcr;
      mean = cpl_vector_get_mean(tot);
      stdev = cpl_vector_get_stdev(tot);
    } /* else */

    double hmean = cpl_vector_get_mean(hori),
           vmean = cpl_vector_get_mean(vert),
           hstdev = cpl_vector_get_stdev(hori),
           vstdev = cpl_vector_get_stdev(vert);
    cpl_msg_debug(__func__, "quadrant %1hhu: %.3f+/-%.3f (h=%.1f+/-%.1f; "
                  "v=%.1f+/-%.1f; nrej=%d) [%s]", n, mean, stdev,
                  hmean, hstdev, vmean, vstdev, nrej, aRejection);
    /* cross check vertical and horizontal against total */
    double lo = mean - stdev,
           hi = mean + stdev;
    if (hmean < lo || hmean > hi) {
      cpl_msg_warning(__func__, "Horizontal overscan differs from total "
                      "(%.3f outside %.3f+/-%.3f)", hmean, mean, stdev);
    }
    if (vmean < lo || vmean > hi) {
      cpl_msg_warning(__func__, "Vertical overscan differs from total "
                      "(%.3f outside %.3f+/-%.3f)", vmean, mean, stdev);
    }

    char *keyword = cpl_sprintf(MUSE_HDR_OVSC_MEAN, n);
    cpl_propertylist_append_float(aImage->header, keyword, mean);
    cpl_free(keyword);
    keyword = cpl_sprintf(MUSE_HDR_OVSC_STDEV, n);
    cpl_propertylist_append_float(aImage->header, keyword, stdev);
    cpl_free(keyword);

    cpl_vector_delete(hori);
    cpl_vector_delete(vert);
    cpl_vector_delete(tot);
    cpl_free(w);
  } /* for n (quadrants) */

  return CPL_ERROR_NONE;
} /* muse_quadrants_overscan_stats() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Compare overscan statistics of all quadrants to those of reference
           image.
  @param   aImage      muse_image that holds the header to check
  @param   aRefImage   muse_image that holds the header to use as reference
  @param   aSigma      sigma to use for overscan comparison
  @return  CPL_TRUE on success or CPL_FALSE on error or for bad comparison.

  @error{set CPL_ERROR_NULL_INPUT\, return CPL_FALSE,
         if aImage or aRefImage or their header components are NULL}
 */
/*---------------------------------------------------------------------------*/
cpl_boolean
muse_quadrants_overscan_check(muse_image *aImage, muse_image *aRefImage,
                              double aSigma)
{
  cpl_ensure(aImage && aImage->header && aRefImage && aRefImage->header,
             CPL_ERROR_NULL_INPUT, CPL_FALSE);

  int rc = CPL_TRUE;
  unsigned char n; /* quadrant counter */
  for (n = 1; n <= 4; n++) {
    /* create correct header keyword names */
    char *keywordmean = cpl_sprintf(MUSE_HDR_OVSC_MEAN, n),
         *keywordstdev = cpl_sprintf(MUSE_HDR_OVSC_STDEV, n);

    /* overscan stats for reference image */
    float refmean = cpl_propertylist_get_float(aRefImage->header, keywordmean),
          refstdev = cpl_propertylist_get_float(aRefImage->header, keywordstdev),
          hilimit = refmean + aSigma * refstdev,
          lolimit = refmean - aSigma * refstdev;

    float mean = cpl_propertylist_get_float(aImage->header, keywordmean),
          stdev = cpl_propertylist_get_float(aImage->header, keywordstdev);
    if (mean > hilimit || mean < lolimit) {
      const char *fn = cpl_propertylist_get_string(aImage->header, MUSE_HDR_TMP_FN),
                 *reffn = cpl_propertylist_get_string(aRefImage->header, MUSE_HDR_TMP_FN);
      cpl_msg_warning(__func__, "Overscan of quadrant %1u of image [%s] "
                      "(%.3f+/-%.3f) differs from reference image [%s] "
                      "(%.3f+/-%.3f)!", n, fn, mean, stdev, reffn, refmean,
                      refstdev);
      rc = CPL_FALSE;
    }

    cpl_free(keywordmean);
    cpl_free(keywordstdev);
  } /* for n (quadrants) */

  return rc;
} /* muse_quadrants_overscan_check() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Adapt bias level to reference image using overscan statistics.
  @param   aImage      muse_image to be corrected
  @param   aRefImage   muse_image that holds the header to use as reference
  @return  CPL_ERROR_NONE on success or another CPL error code on failure.

  This offsets the data values of aImage according to the mean overscan level
  difference between aImage and aRefImage. The variance of aImage is adjusted
  using the standard deviation of the overscan values of both images.

  @error{return CPL_ERROR_NULL_INPUT,
         if aImage or aRefImage or their header components are NULL}
 */
/*---------------------------------------------------------------------------*/
cpl_error_code
muse_quadrants_overscan_correct(muse_image *aImage, muse_image *aRefImage)
{
  cpl_ensure_code(aImage && aImage->header && aRefImage && aRefImage->header,
                  CPL_ERROR_NULL_INPUT);

  unsigned char n; /* quadrant counter */
  for (n = 1; n <= 4; n++) {
    char *keywordmean = cpl_sprintf(MUSE_HDR_OVSC_MEAN, n),
         *keywordstdev = cpl_sprintf(MUSE_HDR_OVSC_STDEV, n);
    float refmean = cpl_propertylist_get_float(aRefImage->header, keywordmean),
          refstdev = cpl_propertylist_get_float(aRefImage->header, keywordstdev),
          mean = cpl_propertylist_get_float(aImage->header, keywordmean),
          stdev = cpl_propertylist_get_float(aImage->header, keywordstdev);
#if 0
    cpl_msg_debug(__func__, "ref=%f+/-%f, image=%f+/-%f", refmean, refstdev,
                  mean, stdev);
#endif
    cpl_size *w = muse_quadrants_get_window(aImage, n);

    /* subtract difference from this quadrant of aImage->data */
    /* get the data for this quadrant */
    cpl_image *data = cpl_image_extract(aImage->data, w[0], w[2], w[1], w[3]);
    cpl_image_add_scalar(data, refmean - mean);
    /* copy the data back into the original image */
    cpl_image_copy(aImage->data, data, w[0], w[2]);
    cpl_image_delete(data);

    /* somehow add the stdev to this quadrant of aImage->stat in quadrature */
    cpl_image *stat = cpl_image_extract(aImage->stat, w[0], w[2], w[1], w[3]);
    /* use the gain to correct the variances, as in muse_image_variance_create() */
    double gain = muse_pfits_get_gain(aImage->header, n); /* in count/adu */
#if 0
    cpl_msg_debug(__func__, "variance: %f", (refstdev*refstdev + stdev*stdev) / gain);
#endif
    cpl_image_add_scalar(stat, (refstdev*refstdev + stdev*stdev) / gain);
    cpl_image_copy(aImage->stat, stat, w[0], w[2]);
    cpl_image_delete(stat);

    /* update the overscans */
    cpl_propertylist_update_float(aImage->header, keywordmean, refmean);

    cpl_free(w);
    cpl_free(keywordmean);
    cpl_free(keywordstdev);
  } /* for n (quadrants) */

  return CPL_ERROR_NONE;
} /* muse_quadrants_overscan_correct() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Correct quadrants by polynomial representation of vertical overscan.
  @param   aImage    muse_image to be corrected
  @param   aIgnore   number of pixels to ignore at the inner edge of the overscans
  @param   aOrder    maximum polynomial order to use
  @param   aSigma    rejection sigma level for the iterative fit
  @param   aFRMS     improvement limit in RMS
  @param   aFChiSq   improvement limit in chi**2
  @return  CPL_ERROR_NONE on success or another CPL error code on failure.

  This function modifies the input image (the data component) and the header,
  trying to remove possible large-scale vertical bias structure. The steps
  involved are:
  - check all relevant header keywords related to the CCD regions
  - loop through the four quadrants:
    . set each valid input pixel and vertical position in the vertical overscan
      region into CPL structures
    . exclude pixels closer to the illuminated CCD region than aIgnore pixels
    . use the CPL structures to fit 1D polynomials using subsequently higher
      polynomial order
    . aSigma is the sigma-level used for iterative rejection of points in each
      fit
    . the fit result with the higher order is compared to the previously lower
      order in terms of RMS and reduced chi**2: if the improvement in RMS is
      less than aFRMS and the improvement in chi**2 is less than aFChiSq, then
      the procedure is stopped; otherwise the next higher order fit is attempted
    . the resulting polynomial is then evaluated at each vertical position in
      the quadrant and that value is subtracted from along the whole row
      (including all pre- and overscan regions)
  The polynomial parameters used are saved in the FITS header of aImage: the
  macros MUSE_HDR_OVSC_PNC and MUSE_HDR_OVSC_PY are used to store the number of
  coefficients (polynomial order + 1) and the coefficients, respectively, for
  each quadrant.

  @error{return CPL_ERROR_NULL_INPUT,
         aImage or its data\, dq\, or header components are NULL}
  @error{return CPL_ERROR_ILLEGAL_INPUT, aFRMS and/or aFChiSq are not > 1.}
  @error{return CPL_ERROR_BAD_FILE_FORMAT,
         the quadrant layout in aImage is non-standard\, i.e. muse_quadrants_verify() fails}
  @error{return CPL_ERROR_INCOMPATIBLE_INPUT,
         one of the quadrant 1 parameters is missing from aImage or non-positive}
  @error{return CPL_ERROR_ILLEGAL_INPUT,
         getting overscan regions\, i.e. muse_quadrants_overscan_get_window() fails\, possibly due to too large aIgnore}
  @error{return CPL_ERROR_INCOMPATIBLE_INPUT,
         unexpected output port position in aImage}
 */
/*---------------------------------------------------------------------------*/
cpl_error_code
muse_quadrants_overscan_polyfit_vertical(muse_image *aImage, unsigned aIgnore,
                                         unsigned char aOrder, double aSigma,
                                         const double aFRMS, double aFChiSq)
{
  cpl_ensure_code(aImage && aImage->data && aImage->dq && aImage->header,
                  CPL_ERROR_NULL_INPUT);
  cpl_ensure_code(aFRMS > 1. && aFChiSq > 1., CPL_ERROR_ILLEGAL_INPUT);
  /* ensure that the region sizes are the same for all quadrants */
  cpl_ensure_code(muse_quadrants_verify(aImage->header),
                  CPL_ERROR_BAD_FILE_FORMAT);

  /* get output sizes for data regions, and pre- and overscans */
  int nx = cpl_image_get_size_x(aImage->data),
      binx = muse_pfits_get_binx(aImage->header),
      biny = muse_pfits_get_biny(aImage->header),
      outnx = muse_pfits_get_out_nx(aImage->header, 1) / binx,
      outny = muse_pfits_get_out_ny(aImage->header, 1) / biny,
      prex = muse_pfits_get_out_prescan_x(aImage->header, 1) / binx,
      prey = muse_pfits_get_out_prescan_y(aImage->header, 1) / biny,
      overx = muse_pfits_get_out_overscan_x(aImage->header, 1) / binx,
      overy = muse_pfits_get_out_overscan_y(aImage->header, 1) / biny;
  cpl_ensure_code(outnx > 0 && outny > 0 && overx > 0 && overy > 0 &&
                  prex > 0 && prey > 0, CPL_ERROR_INCOMPATIBLE_INPUT);
  unsigned char ifu = muse_utils_get_ifu(aImage->header);

  /* Check that the overscan windows make sense for all quadrants;  *
   * do this for all quadrants before everything else, so that we   *
   * don't end up with a partially corrected image! This implicitly *
   * checks, if the input aIgnore value is usable.                  */
  unsigned char n; /* quadrant counter */
  for (n = 1; n <= 4; n++) {
    cpl_size *w = muse_quadrants_overscan_get_window(aImage, n, aIgnore);
    cpl_ensure_code(w, CPL_ERROR_ILLEGAL_INPUT);
    cpl_free(w);
  } /* for n (quadrants) */

  int debug = 0;
  if (getenv("MUSE_DEBUG_QUADRANTS")) {
    debug = atoi(getenv("MUSE_DEBUG_QUADRANTS"));
  }

  float *data = cpl_image_get_data_float(aImage->data);
  const int *dq = cpl_image_get_data_int_const(aImage->dq);
  for (n = 1; n <= 4; n++) {
    cpl_size *w = muse_quadrants_overscan_get_window(aImage, n, aIgnore);
    /* over vertical overscan only, indices 4 - 7 */
    int nvert = (w[5] - w[4] + 1) * (w[7] - w[6] + 1);
    cpl_matrix *pos = cpl_matrix_new(1, nvert);
    cpl_vector *val = cpl_vector_new(nvert);
    int i, j, ivert = 0;
    for (i = w[4] - 1; i < w[5]; i++) {
      for (j = w[6] - 1; j < w[7]; j++) {
        if (dq[i + j*nx]) { /* exclude pixels with bad status */
          continue;
        }
        cpl_matrix_set(pos, 0, ivert, j + 1);
        cpl_vector_set(val, ivert++, data[i + j*nx]);
      } /* for j */
    } /* for i */
    cpl_matrix_set_size(pos, 1, ivert);
    cpl_vector_set_size(val, ivert);

    /* Fit a constant first, and compute the RMS and chi**2 of the fit; *
     * do not iterate yet, so pass sigma = FLT_MAX                      */
    double rms1, rms2, chisq1, chisq2;
    cpl_polynomial *fit0 = muse_utils_iterate_fit_polynomial(pos, val, NULL, NULL, 0,
                                                             FLT_MAX, &rms1, &chisq1),
                   *fit = fit0;
    rms1 = sqrt(rms1);
    if (debug) {
      cpl_msg_debug(__func__, "IFU %2hhu Q%1hhu order 0: %f / %f, p(%"
                    CPL_SIZE_FORMAT")=%f .. p(%"CPL_SIZE_FORMAT")=%f",
                    ifu, n, rms1, chisq1,
                    w[6], cpl_polynomial_eval_1d(fit, w[6], NULL),
                    w[7], cpl_polynomial_eval_1d(fit, w[7], NULL));
    } /* if debug */
    /* now see, how much a higher order improves the fit */
    unsigned char norder;
    for (norder = 1; norder <= aOrder; norder++) {
      cpl_polynomial *fit2 = muse_utils_iterate_fit_polynomial(pos, val, NULL, NULL,
                                                               norder, FLT_MAX, &rms2,
                                                               &chisq2);
      rms2 = sqrt(rms2);
      /* improvement factors in RMS and chi**2 */
      double frms = rms1 / rms2,
             fchisq = chisq1 / chisq2;
      if (debug) {
        cpl_msg_debug(__func__, "IFU %2hhu Q%1hhu order %hhu: %f / %f, %f / %f,"
                      " p(%"CPL_SIZE_FORMAT")=%f .. p(%"CPL_SIZE_FORMAT")=%f",
                      ifu, n, norder, rms2, chisq2, frms, fchisq,
                      w[6], cpl_polynomial_eval_1d(fit2, w[6], NULL),
                      w[7], cpl_polynomial_eval_1d(fit2, w[7], NULL));
      } /* if debug */
      if (norder == aOrder || (frms < aFRMS && fchisq < aFChiSq)) {
        fit = fit2;
        break;
      }
      cpl_polynomial_delete(fit2);
      rms1 = rms2;
      chisq1 = chisq2;
    } /* for norder */
    /* now use the selected order to fit the final solution, *
     * iteratively, using the given sigma level              */
    unsigned char order = cpl_polynomial_get_degree(fit);
    cpl_polynomial_delete(fit);
    fit = muse_utils_iterate_fit_polynomial(pos, val, NULL, NULL, order,
                                            aSigma, &rms2, &chisq2);
    rms2 = sqrt(rms2);
    if (debug) {
      cpl_msg_debug(__func__, "IFU %2hhu Q%1hhu order %hhu (final, sigma %.3f):"
                    " %f / %f, p(%"CPL_SIZE_FORMAT")=%f .. p(%"CPL_SIZE_FORMAT")=%f",
                    ifu, n, order, aSigma, rms2, chisq2,
                    w[6], cpl_polynomial_eval_1d(fit, w[6], NULL),
                    w[7], cpl_polynomial_eval_1d(fit, w[7], NULL));
      if (debug > 1) {
        char *fntmp = cpl_strdup(cpl_propertylist_get_string(aImage->header,
                                                             MUSE_HDR_TMP_FN)),
             *dotfits = strstr(fntmp, ".fits");
        if (dotfits) {
          *dotfits = '\0'; /* strip off the .fits extension */
        }
        char *fn = cpl_sprintf("%s_vpoly_IFU%02hhu_Q%1hhu.ascii", fntmp, ifu, n);
        cpl_msg_debug(__func__, "Saving extra debug data to \"%s\"", fn);
        cpl_free(fntmp);
        FILE *fp = fopen(fn, "w");
        cpl_free(fn);
        fprintf(fp, "# IFU %2hhu Q%1hhu order %hhu (final, sigma %.3f): %f / %f,"
                " p(%"CPL_SIZE_FORMAT")=%f .. p(%"CPL_SIZE_FORMAT")=%f\n",
                ifu, n, order, aSigma, rms2, chisq2,
                w[6], cpl_polynomial_eval_1d(fit, w[6], NULL),
                w[7], cpl_polynomial_eval_1d(fit, w[7], NULL));
        /* output the polynomial fit to the file as well */
        fprintf(fp, "# p(y) =");
        for (i = 0; i <= order; i++) {
          cpl_size pows[] = { i };
          fprintf(fp, " + (%.10e) * y**%d", cpl_polynomial_get_coeff(fit, pows),
                  i);
        }
        fprintf(fp, "\n\n# y\tvalue\n");
        /* output all (non-rejected) data points to the file */
        for (i = 0; i < cpl_vector_get_size(val); i++) {
          fprintf(fp, "%4d\t%f\n", (int)cpl_matrix_get(pos, 0, i),
                  cpl_vector_get(val, i));
        } /* for i (all datapoints) */
        fclose(fp);
      } /* if debug > 1 */
    } /* if debug */
    cpl_matrix_delete(pos);
    cpl_vector_delete(val);
    char *kw = cpl_sprintf(MUSE_HDR_OVSC_PNC, n);
    cpl_propertylist_append_int(aImage->header, kw, order + 1);
    cpl_free(kw);
    for (norder = 0; norder <= order; norder++) {
      cpl_size pows = norder;
      kw = cpl_sprintf(MUSE_HDR_OVSC_PY, n, norder);
      cpl_propertylist_append_double(aImage->header, kw,
                                     cpl_polynomial_get_coeff(fit, &pows));
      cpl_free(kw);
    } /* for norder */

    /* Now create a vector containing a sampling from the polynomial,    *
     * and subtract this from every column of the given quadrant window. */
    int outx = muse_pfits_get_out_output_x(aImage->header, n),
        outy = muse_pfits_get_out_output_y(aImage->header, n),
        i1 = outx == 1 ? 1 : prex + outnx + overx + 1,
        i2 = outx == 1 ? prex + outnx + overx : 2 * (prex + outnx + overx),
        j1 = outy == 1 ? 1 : prey + outny + overy + 1,
        j2 = outy == 1 ? prey + outny + overy : 2 * (prey + outny + overy);
    if (debug > 1) {
      cpl_msg_debug(__func__, "quad %1hhu fill region [%d:%d,%d:%d]", n,
                    i1, i2, j1, j2);
    }
    /* Evaluate the polynomial at every vertical position in the       *
     * quadrant, then subtract it from the whole row in that quadrant. */
    for (j = j1 - 1; j < j2; j++) {
      double subval = cpl_polynomial_eval_1d(fit, j + 1, NULL);
      for (i = i1 - 1; i < i2; i++) {
        data[i + j*nx] -= subval;
      } /* for i */
    } /* for j */

    if (fit != fit0) {
      cpl_polynomial_delete(fit);
    }
    cpl_polynomial_delete(fit0);
    cpl_free(w);
  } /* for n (quadrants) */

  return CPL_ERROR_NONE;
} /* muse_quadrants_overscan_polyfit_vertical() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Determine the overscan windows of a given quadrant on the CCD.
  @param   aImage      muse_image that holds the necessary data
  @param   aQuadrant   the quadrant number
  @param   aIgnore     number of pixels to ignore at the inner edge of the
                       overscans
  @return  the array of eight window coordinates or NULL on error

  The returned window array contains eight values, in the order xmin, xmax,
  ymin, ymax for the horizontal overscan region (indices 0 - 3), and then xmin,
  xmax, ymin, ymax for the vertical overscan region (indices 4 - 7). The small
  square where both overscans overlap is counted as part of the vertical
  overscan.

  The returned array has to be deallocated using cpl_free() after use.

  Since trimmed data does not contain header entries about pre- and overscans
  any more, such data will is incompatible input and will be rejected.

  @note This function has to assume a standard quadrant layout, one should
        use muse_quadrants_verify() to check this beforehand!

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         if aImage or its data or header component are NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         an invalid quadrant number was given (>4 or <1)}
  @error{set CPL_ERROR_INCOMPATIBLE_INPUT\, return NULL,
         one of the quadrant parameters is missing or non-positive\, or unexpected output port position was found in aImage}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         aIgnore as large as the horizontal overscan width or larger}
 */
/*---------------------------------------------------------------------------*/
cpl_size *
muse_quadrants_overscan_get_window(const muse_image *aImage,
                                   unsigned char aQuadrant,
                                   unsigned int aIgnore)
{
  cpl_ensure(aImage && aImage->data && aImage->header, CPL_ERROR_NULL_INPUT,
             NULL);
  cpl_ensure(aQuadrant >= 1 && aQuadrant <= 4, CPL_ERROR_ILLEGAL_INPUT, NULL);
  /* get output sizes for data regions, and pre- and overscans */
  cpl_errorstate state = cpl_errorstate_get();
  int binx = muse_pfits_get_binx(aImage->header),
      biny = muse_pfits_get_biny(aImage->header),
      outnx = muse_pfits_get_out_nx(aImage->header, aQuadrant) / binx,
      outny = muse_pfits_get_out_ny(aImage->header, aQuadrant) / biny,
      prex = muse_pfits_get_out_prescan_x(aImage->header, aQuadrant) / binx,
      prey = muse_pfits_get_out_prescan_y(aImage->header, aQuadrant) / biny,
      overx = muse_pfits_get_out_overscan_x(aImage->header, aQuadrant) / binx,
      overy = muse_pfits_get_out_overscan_y(aImage->header, aQuadrant) / biny,
      portx = muse_pfits_get_out_output_x(aImage->header, aQuadrant),
      porty = muse_pfits_get_out_output_y(aImage->header, aQuadrant);
#if 0
  cpl_msg_debug(__func__, "%d/%d, out %d/%d, pre %d/%d, over %d/%d, port %d/%d",
                binx, biny, outnx, outny, prex, prey, overx, overy, portx, porty);
#endif
  cpl_ensure(cpl_errorstate_is_equal(state) &&
             outnx > 0 && outny > 0 && overx > 0 && overy > 0 &&
             prex >= 0 && prey >= 0 && binx > 0 && biny > 0 &&
             (portx == 1 || portx == kMuseOutputXRight) &&
             (porty == 1 || porty == kMuseOutputYTop),
             CPL_ERROR_INCOMPATIBLE_INPUT, NULL);
  cpl_ensure(aIgnore < (unsigned int)overx, CPL_ERROR_ILLEGAL_INPUT, NULL);

  /* horizontal overscan region: x1, x2, y1, y2 (indices: 0 - 3) *
   * vertical overscan region:   x1, x2, y1, y2 (indices: 4 - 7) */
  cpl_size *over = cpl_calloc(8, sizeof(cpl_size));
  if (portx == 1) {
    over[0] = prex + 1;
    over[1] = prex + outnx;
    over[4] = prex + outnx + 1 + aIgnore;
    over[5] = prex + outnx + overx;
  } else { /* portx == kMuseOutputXRight */
    over[0] = prex + outnx + 2*overx + 1;
    over[1] = prex + 2*outnx + 2*overx;
    over[4] = prex + outnx + overx + 1;
    over[5] = prex + outnx + 2*overx - aIgnore;
  }
  if (porty == 1) {
    over[2] = prey + outny + 1 + aIgnore;
    over[3] = prey + outny + overy;
    over[6] = prey + 1;
    over[7] = prey + outny + overy;
  } else { /* porty == kMuseOutputYTop */
    over[2] = prey + outny + overy + 1;
    over[3] = prey + outny + 2*overy - aIgnore;
    over[6] = prey + outny + overy + 1;
    over[7] = prey + 2*outny + 2*overy;
  }

  if (getenv("MUSE_DEBUG_QUADRANTS") &&
      atoi(getenv("MUSE_DEBUG_QUADRANTS")) > 0) {
    cpl_msg_debug(__func__, "Quadrant %hhu overscan regions: "
                  "[%"CPL_SIZE_FORMAT":%"CPL_SIZE_FORMAT
                  ",%"CPL_SIZE_FORMAT":%"CPL_SIZE_FORMAT"] and "
                  "[%"CPL_SIZE_FORMAT":%"CPL_SIZE_FORMAT
                  ",%"CPL_SIZE_FORMAT":%"CPL_SIZE_FORMAT"]", aQuadrant,
                  over[0], over[1], over[2], over[3],
                  over[4], over[5], over[6], over[7]);
  } /* if debug */
  return over;
} /* muse_quadrants_overscan_get_window() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Trim the input image of pre- and over-scan regions of all quadrants.
  @param   aImage   muse_image that holds the image to trim
  @return  the trimmed image or NULL on error

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         if aImage or its data or header component are NULL}
  @error{set CPL_ERROR_DATA_NOT_FOUND\, return NULL,
         FITS headers are missing required keywords}
  @error{set CPL_ERROR_INCOMPATIBLE_INPUT\, return NULL,
         FITS headers contain incompatible read-out port setup (order of quadrants\, differing sizes)}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         FITS header in input lead to computation of too large or non-positive output sizes}
 */
/*---------------------------------------------------------------------------*/
muse_image *
muse_quadrants_trim_image(muse_image *aImage)
{
  cpl_ensure(aImage && aImage->data && aImage->header, CPL_ERROR_NULL_INPUT,
             NULL);
  cpl_boolean debug = CPL_FALSE;
  if (getenv("MUSE_DEBUG_QUADRANTS")) {
    debug = atoi(getenv("MUSE_DEBUG_QUADRANTS")) > 0;
  }

  /* output image size */
  int xout = 0, yout = 0;
  /* arrays of input data region sizes, and output port locations */
  int nx[4], ny[4], portx[4], porty[4];
  /* binning to take into account */
  int binx = muse_pfits_get_binx(aImage->header),
      biny = muse_pfits_get_biny(aImage->header);

  /* get data region for each quadrant and compute the size of the output image */
  unsigned char n; /* quadrant counter */
  for (n = 1; n <= 4; n++) {
    nx[n-1] = muse_pfits_get_out_nx(aImage->header, n) / binx;
    ny[n-1] = muse_pfits_get_out_ny(aImage->header, n) / biny;
    portx[n-1] = muse_pfits_get_out_output_x(aImage->header, n);
    porty[n-1] = muse_pfits_get_out_output_y(aImage->header, n);
    /* check values to see if the relevant headers were present, otherwise abort */
    if (nx[n-1] < 0 || ny[n-1] < 0 || portx[n-1] < 0 || porty[n-1] < 0) {
      cpl_msg_error(__func__, "FITS headers necessary for trimming are missing "
                    "from quadrant %1d: NX=%d, NY=%d at OUT X=%d/OUT Y=%d", n,
                    nx[n-1], ny[n-1], portx[n-1], porty[n-1]);
      cpl_error_set(__func__, CPL_ERROR_DATA_NOT_FOUND);
      return NULL;
    }
    /* check values to see if the outputs are valid, otherwise abort */
    if ((portx[n-1] != 1 && portx[n-1] != kMuseOutputXRight) ||
        (porty[n-1] != 1 && porty[n-1] != kMuseOutputYTop)) {
      cpl_msg_error(__func__, "FITS headers necessary for trimming are "
                    "unsupported for quadrant %1d: OUT X=%d/OUT Y=%d", n,
                    portx[n-1], porty[n-1]);
      cpl_error_set(__func__, CPL_ERROR_INCOMPATIBLE_INPUT);
      return NULL;
    }

    /* use the left two quadrants to count the output height */
    if (portx[n-1] == 1) {
      yout += ny[n-1];
    }
    /* use the bottom two quadrants to count the output width */
    if (porty[n-1] == 1) {
      xout += nx[n-1];
    }
  } /* for n (quadrants) */

  /* now we should have a valid output size */
  int x_size = cpl_image_get_size_x(aImage->data),
      y_size = cpl_image_get_size_y(aImage->data);
  if (xout > x_size || yout > y_size) {
    cpl_msg_error(__func__, "output size (%dx%d) is larger than input size "
                  "(%dx%d): wrong binning?!", xout, yout, x_size, y_size);
    cpl_error_set(__func__, CPL_ERROR_ILLEGAL_INPUT);
    return NULL;
  }
  if (debug) {
    cpl_msg_debug(__func__, "output size %dx%d", xout, yout);
  }
  cpl_ensure(xout > 0 && yout > 0, CPL_ERROR_ILLEGAL_INPUT, NULL);

  /* verify that all the data regions have the same size */
  for (n = 1; n < 4; n++) {
    if (nx[n] != nx[0] || ny[n] != ny[0]) {
      cpl_msg_error(__func__, "Data region of quadrant %d is different from "
                    "quadrant 1!", n+1);
      cpl_error_set(__func__, CPL_ERROR_INCOMPATIBLE_INPUT);
      return NULL;
    }
  }

  /* create the trimmed output image and its components */
  muse_image *trimmed = muse_image_new();
  trimmed->data = cpl_image_new(xout, yout, CPL_TYPE_FLOAT);
  /* only create the other two components if they exist in the input image */
  if (aImage->dq) {
    trimmed->dq = cpl_image_new(xout, yout, CPL_TYPE_INT);
  }
  if (aImage->stat) {
    trimmed->stat = cpl_image_new(xout, yout, CPL_TYPE_FLOAT);
  }

  /* copy the input header but erase those that are no longer relevant */
  trimmed->header = cpl_propertylist_duplicate(aImage->header);
  cpl_propertylist_erase_regexp(trimmed->header,
                                "^NAXIS|^DATASUM$|^DATAMIN$|^DATAMAX$|^DATAMD5$|"
                                "^ESO DET OUT.*PRSC|^ESO DET OUT.*OVSC",
                                0);

  /* copy data regions (of all three extensions, if they exist) *
   * and for all quadrants into the output image                */
  for (n = 1; n <= 4; n++) {
    /* read properties from input header */
    int x_prescan = muse_pfits_get_out_prescan_x(aImage->header, n) / binx,
        y_prescan = muse_pfits_get_out_prescan_y(aImage->header, n) / biny;

    /* determine the data regions to extract and the target output positions */
    int x1 = 0, x2 = 0, y1 = 0, y2= 0,
        xtarget = 0, ytarget = 0;

    /* quadrant at left or at right */
    if (portx[n-1] == 1) {
#if 0
      cpl_msg_debug(__func__, "left quadrant (OUT%d)", n);
#endif
      x1 = x_prescan + 1;
      x2 = x_prescan + nx[0];
      xtarget = 1;
    } else if (portx[n-1] == kMuseOutputXRight) {
#if 0
      cpl_msg_debug(__func__, "right quadrant (OUT%d)", n);
#endif
      x1 = x_size - x_prescan - nx[0] + 1;
      x2 = x_size - x_prescan;
      xtarget = nx[0] + 1;
    }

    /* quadrant at bottom or at top */
    if (porty[n-1] == 1) {
#if 0
      cpl_msg_debug(__func__, "bottom quadrant (OUT%d)", n);
#endif
      y1 = y_prescan + 1;
      y2 = y_prescan + ny[0];
      ytarget = 1;
    } else if (porty[n-1] == kMuseOutputYTop) {
#if 0
      cpl_msg_debug(__func__, "top quadrant (OUT%d)", n);
#endif
      y1 = y_size - y_prescan - ny[0] + 1;
      y2 = y_size - y_prescan;
      ytarget = ny[0] + 1;
    }

    /* copy data and maybe extensions */
    cpl_image *image = cpl_image_extract(aImage->data, x1, y1, x2, y2);
    if (debug) {
      cpl_msg_debug(__func__, "port at %d,%d: %d,%d - %d,%d, extracted: "
                    "%"CPL_SIZE_FORMAT"x%"CPL_SIZE_FORMAT" -> %d,%d",
                    portx[n-1], porty[n-1], x1,y1, x2,y2,
                    cpl_image_get_size_x(image), cpl_image_get_size_y(image),
                    xtarget, ytarget);
    }
    cpl_image_copy(trimmed->data, image, xtarget, ytarget);
    cpl_image_delete(image);
    if (aImage->dq) {
      image = cpl_image_extract(aImage->dq, x1, y1, x2, y2);
      cpl_image_copy(trimmed->dq, image, xtarget, ytarget);
      cpl_image_delete(image);
    }
    if (aImage->stat) {
      image = cpl_image_extract(aImage->stat, x1, y1, x2, y2);
      cpl_image_copy(trimmed->stat, image, xtarget, ytarget);
      cpl_image_delete(image);
    }
  } /* for n (quadrants) */

#if 0
  muse_image_save(aImage, "input.fits");
  muse_image_save(trimmed, "trimmed.fits");
#endif

  return trimmed;
} /* muse_quadrants_trim_image() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Convert coordinates of a trimmed image to raw-image coordinates.
  @param   aHeader   list of FITS headers to check
  @param   aX        the horizontal position
  @param   aY        the vertical position
  @return  CPL_ERROR_NONE on success, another CPL error code on failure

  @warning Handling of an input header is not yet implemented!

  @error{return CPL_ERROR_NULL_INPUT, both aX and aY are NULL}
  @error{assume standard layout, aHeader is NULL}
 */
/*---------------------------------------------------------------------------*/
cpl_error_code
muse_quadrants_coords_to_raw(cpl_propertylist *aHeader, int *aX, int *aY)
{
  cpl_ensure_code(aX || aY, CPL_ERROR_NULL_INPUT);
  if (!aHeader) {
    /* assume standard quadrants */
    if (aX) {
      int xraw = *aX + kMusePreOverscanSize; /* add one prescan in any case */
      if (*aX > kMuseOutputXRight/2) {
        xraw += 2 * kMusePreOverscanSize; /* add the two overscans, too */
      } /* if in right quadrant */
      *aX = xraw;
    } /* if aX */
    if (aY) {
      int yraw = *aY + kMusePreOverscanSize;
      if (*aY > kMuseOutputYTop/2) {
        yraw += 2 * kMusePreOverscanSize;
      } /* if in top quadrant */
      *aY = yraw;
    } /* if aY */
    return CPL_ERROR_NONE;
  }
  /* XXX do futher checks if the header was passed */
  return CPL_ERROR_NONE;
} /* muse_quadrants_coords_to_raw() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Verify that quadrant locations and sizes meet the expectations.
  @param   aHeader   list of FITS headers to check
  @return  CPL_TRUE when verification succeeded, CPL_FALSE otherwise or on error

  The expected quadrant locations are

     4(=H)   3(=G)

     1(=E)   2(=F)

  Note that INM data has ports G and H switched.

  @error{set CPL_ERROR_NULL_INPUT\, return CPL_FALSE,
         the input header is NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return CPL_FALSE,
         the order of the ports is unexpected}
  @error{set CPL_ERROR_INCOMPATIBLE_INPUT\, return CPL_FALSE,
         the sizes of the quadrants are not equal}
 */
/*---------------------------------------------------------------------------*/
cpl_boolean
muse_quadrants_verify(cpl_propertylist *aHeader)
{
  cpl_ensure(aHeader, CPL_ERROR_NULL_INPUT, CPL_FALSE);

  int portx[4], porty[4], nx[4], ny[4],
      prex[4], prey[4], overx[4], overy[4];

  /* get the output position for each quadrant */
  int binx = muse_pfits_get_binx(aHeader),
      biny = muse_pfits_get_biny(aHeader);
  unsigned char n; /* quadrant counter */
  for (n = 1; n <= 4; n++) {
    portx[n-1] = muse_pfits_get_out_output_x(aHeader, n);
    porty[n-1] = muse_pfits_get_out_output_y(aHeader, n);
    nx[n-1] = muse_pfits_get_out_nx(aHeader, n) / binx;
    ny[n-1] = muse_pfits_get_out_ny(aHeader, n) / biny;
    prex[n-1] = muse_pfits_get_out_prescan_x(aHeader, n) / binx;
    prey[n-1] = muse_pfits_get_out_prescan_y(aHeader, n) / biny;
    overx[n-1] = muse_pfits_get_out_overscan_x(aHeader, n) / binx;
    overy[n-1] = muse_pfits_get_out_overscan_y(aHeader, n) / biny;
#if 0
    cpl_msg_debug(__func__, "quadrant %1hhu: port=%d,%d, n=%d,%d, pre=%d,%d, over=%d,%d",
                  n+1, portx[n-1], porty[n-1], nx[n-1], ny[n-1],
                  prex[n-1], prey[n-1], overx[n-1], overy[n-1]);
#endif
  }

  cpl_ensure(portx[0] < portx[1], CPL_ERROR_ILLEGAL_INPUT, CPL_FALSE);
  /* XXX don't check the x locations for the INM */
  if (!cpl_propertylist_has(aHeader, "INMMODEL")) {
    cpl_ensure(portx[0] < portx[2], CPL_ERROR_ILLEGAL_INPUT, CPL_FALSE);
    cpl_ensure(portx[0] == portx[3], CPL_ERROR_ILLEGAL_INPUT, CPL_FALSE);
  }
  cpl_ensure(porty[0] == porty[1], CPL_ERROR_ILLEGAL_INPUT, CPL_FALSE);
  cpl_ensure(porty[0] < porty[2], CPL_ERROR_ILLEGAL_INPUT, CPL_FALSE);
  cpl_ensure(porty[0] < porty[3], CPL_ERROR_ILLEGAL_INPUT, CPL_FALSE);

  for (n = 1; n < 4; n++) {
    cpl_ensure(nx[0] == nx[n] && ny[0] == ny[n], CPL_ERROR_INCOMPATIBLE_INPUT,
               0);
    cpl_ensure(prex[0] == prex[n] && prey[0] == prey[n],
               CPL_ERROR_INCOMPATIBLE_INPUT, CPL_FALSE);
    cpl_ensure(overx[0] == overx[n] && overy[0] == overy[n],
               CPL_ERROR_INCOMPATIBLE_INPUT, CPL_FALSE);
  }

  return CPL_TRUE;
} /* muse_quadrants_verify() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Determine the data window of a given quadrant on the CCD.
  @param   aImage      muse_image that holds the necessary data
  @param   aQuadrant   the quadrant number
  @return  the array of window coordinates or NULL on error

  The returned window array contains four values, in the order xmin, xmax,
  ymin, ymax. It has to be deallocated using cpl_free() after use. For both
  trimmed and untrimmed data, the coordinates give the image window containing
  the data part of the quadrant, i.e. excluding pre- and overscans.

  @error{return CPL_ERROR_NULL_INPUT,
         if aImage or its data or header component are NULL}
  @error{return CPL_ERROR_ILLEGAL_INPUT,
         an invalid quadrant number was given (>4 or < 1)}
 */
/*---------------------------------------------------------------------------*/
cpl_size *
muse_quadrants_get_window(const muse_image *aImage, unsigned char aQuadrant)
{
  cpl_ensure(aImage && aImage->data && aImage->header, CPL_ERROR_NULL_INPUT,
             NULL);
  cpl_ensure(aQuadrant >= 1 && aQuadrant <= 4, CPL_ERROR_ILLEGAL_INPUT, NULL);

  cpl_boolean debug = CPL_FALSE;
  if (getenv("MUSE_DEBUG_QUADRANTS")) {
    debug = atoi(getenv("MUSE_DEBUG_QUADRANTS")) > 0;
  }

  /* get the total image size and the output size for each quadrant */
  int binx = muse_pfits_get_binx(aImage->header),
      biny = muse_pfits_get_biny(aImage->header),
      nx[5], ny[5]; /* element 0 is the total size, others are the quadrants */
  nx[0] = cpl_image_get_size_x(aImage->data);
  ny[0] = cpl_image_get_size_y(aImage->data);
  unsigned char n; /* quadrant counter */
  for (n = 1; n <= 4; n++) {
    nx[n] = muse_pfits_get_out_nx(aImage->header, n) / binx;
    ny[n] = muse_pfits_get_out_ny(aImage->header, n) / biny;
  }

  cpl_size *window = (cpl_size *)cpl_calloc(sizeof(cpl_size), 4);
  switch (aQuadrant) {
  case 1: /* bottom left */
    window[0] = 1;
    window[1] = nx[1];
    window[2] = 1;
    window[3] = ny[1];
    break;
  case 2: /* bottom right */
    window[0] = nx[1] + 1;
    window[1] = nx[1] + nx[2];
    window[2] = 1;
    window[3] = ny[2];
    break;
  case 3: /* top right */
    window[0] = nx[3] + 1; /* XXX switch for INM */
    window[1] = nx[3] + nx[4];
    window[2] = ny[2] + 1;
    window[3] = ny[2] + ny[4];
    break;
  case 4: /* top left */
    window[0] = 1; /* XXX switch for INM */
    window[1] = nx[3];
    window[2] = ny[1] + 1;
    window[3] = ny[1] + ny[3];
    break;
  }

  /* if the total image size is equal to the added size of the output ports, *
   * then the image is trimmed already, and we can return what we have       */
  if ((nx[1] + nx[2]) == nx[0] && (ny[1] + ny[3]) == ny[0]) {
    if (debug) {
      cpl_msg_debug(__func__, "quadrant %d, trimmed: %"CPL_SIZE_FORMAT",%"
                    CPL_SIZE_FORMAT " -> %"CPL_SIZE_FORMAT",%"CPL_SIZE_FORMAT"",
                    aQuadrant, window[0], window[2], window[1], window[3]);
    }
    return window;
  }

  /* as the image seems to be untrimmed, add the pre- and over-scan sizes *
   * of the relevant regions to the positions of the output window        */
  int overx[5], overy[5], /* element 0 is unused, for consistency with above! */
      prex[5], prey[5];
  for (n = 1; n <= 4; n++) {
    prex[n] = muse_pfits_get_out_prescan_x(aImage->header, n) / binx;
    prey[n] = muse_pfits_get_out_prescan_y(aImage->header, n) / biny;
    overx[n] = muse_pfits_get_out_overscan_x(aImage->header, n) / binx;
    overy[n] = muse_pfits_get_out_overscan_y(aImage->header, n) / biny;
  }

  int addx = 0, addy = 0;
  switch (aQuadrant) {
  case 1: /* bottom left, only add prescans */
    addx = prex[1];
    addy = prey[1];
    break;
  case 2: /* bottom right, add prescans, plus twice the horizontal overscan */
    addx = prex[1] + overx[1] + overx[2];
    addy = prey[2];
    break;
  case 3: /* top right, add prescans, plus twice the horizontal+vertical overscan */
    addx = prex[3] + overx[3] + overx[4]; /* XXX switch for INM */
    addy = prey[1] + overy[1] + overy[3];
    break;
  case 4: /* top left, add prescans, plus twice the vertical overscan */
    addx = prex[3]; /* XXX switch for INM */
    addy = prey[2] + overy[2] + overy[4];
    break;
  }
  window[0] += addx;
  window[1] += addx;
  window[2] += addy;
  window[3] += addy;

  if (debug) {
    cpl_msg_debug(__func__, "quadrant %d, not trimmed: %"CPL_SIZE_FORMAT",%"
                  CPL_SIZE_FORMAT " -> %"CPL_SIZE_FORMAT",%"CPL_SIZE_FORMAT,
                  aQuadrant, window[0], window[2], window[1], window[3]);
  }

  return window;
} /* muse_quadrants_get_window() */

/**@}*/
