/* $Id: visir_util_repack.c,v 1.144 2013-09-24 11:09:55 jtaylor Exp $
 *
 * This file is part of the VISIR Pipeline
 * Copyright (C) 2002,2003,2013,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 St, Fifth Floor, Boston, MA  02111-1307  USA
 */

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

/*-----------------------------------------------------------------------------
                                Includes
 -----------------------------------------------------------------------------*/

#include "visir_recipe.h"
#include "irplib_framelist.h"
#include "visir_spc_optmod.h"
#include "visir_spectro.h"
#include "irplib_wcs.h"
#include <cxlist.h>
/* Verify self-sufficiency of CPL header files by including system files last */
#include <string.h>
/* for strcasecmp */
#include <strings.h>
#include <math.h> /* isnan */


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

#ifndef VISIR_UTIL_REPACK_CONAD
#define VISIR_UTIL_REPACK_CONAD 1.1
#endif

#define RECIPE_STRING   "visir_util_repack"

#define VISIR_DRS_CONAD      "ESO DET OUT1 CONAD"
enum nodtype_e {
    ANOD,
    BNOD,
    ABNOD
};


/*-----------------------------------------------------------------------------
                            Private Functions prototypes
 -----------------------------------------------------------------------------*/
typedef enum {
    VISIR_SUB_NOCORRECT = 0,
    VISIR_SUB_CHOPCORRECT = 1,
    VISIR_SUB_CHOPNODCORRECT = 2
} visir_sub_type;

typedef enum {
    VISIR_FRAME_A,
    VISIR_FRAME_B,
    VISIR_FRAME_STATIC,
} visir_frame_type;

/* convinience data structure to hold common data needed
 * to process one frame */
typedef struct {
    int planestart;
    int planeend;
    int trimlow;
    int trimhigh;
    int bkgcorrect;
    cpl_boolean normalize;
    cpl_boolean compress;
    double conad;
    int nframes;
    const irplib_framelist * rawframes;
    int iframe;
    const cpl_parameterlist * parlist;
    const char * tag;
    const cpl_propertylist * plist;
    visir_data_type datatype;
    cpl_type load_type;

    char * onname;
    char * offname;
    const char * remregexp;

    double pthrow;
    double posang;
    double dit;
    int ncycles;
    int nplanes_per_cycle;
    const char * comoffx;
    const char * comoffy;
    double offsetx;
    double offsety;
    double jitterwidth;
    double pxspace;
    double pfov;
    cpl_wcs * wcs;
    visir_spc_resol resol;

    /* only for burst */
    int to_off;
    int halfcycle;

    double bpmthresh;

    int naxis3;
    cpl_boolean is_a;
    const char * procatgon;
    const char * procatgoff;
    cpl_size nx, ny;
    /* initialized from framelist global storage */
    cpl_image * mean_on;
    cpl_size * nmean_on;
    cpl_image * mean_off;
    cpl_size * nmean_off;

    double time_obsstart;
    double time_filewrite;
    /* min and max of a two A/B states to write into a chop nod file */
    double time_min_obsstart;
    double time_max_filewrite;
} repack_framestate;

static cpl_error_code
visir_util_repack_two(const repack_framestate * fstatea,
                      const repack_framestate * fstateb,
                      cpl_frameset * products,
                      cpl_image ** pbpm,
                      cpl_image * nonlinear,
                      const cpl_bivector * lintab);

static inline cpl_error_code
visir_util_repack_check(const cpl_image *,
                        const cpl_imagelist *,
                        const cpl_imagelist *);

#ifdef VISIR_CHAIN
#define cpl_plugin_get_info visir_util_repack_get_info
#endif
cpl_recipe_define(visir_util_repack, VISIR_BINARY_VERSION,
                  "Lars Lundin", PACKAGE_BUGREPORT, "2011", 
                  "Conversion of raw CUBE2 or BURST images to on- and off-cubes",
                  "The files listed in the Set Of Frames (sof-file) "
                  "must be tagged:\n"
                  "VISIR-CUBE2-raw-file.fits " VISIR_UTIL_REPACK_RAW
                  "\nor\n"
                  "VISIR-BURST-raw-file.fits " VISIR_IMG_BURST"\n"
                  "VISIR-BURST-bpm-file.fits " VISIR_CALIB_STATIC_MASK
                  "\nFor BURST data it will remove planes where the chopper "
                  "switched from on <-> off based on the "
                  "timestamps in the header."
                  "\nThe product(s) will have a FITS card\n"
                  "'HIERARCH ESO PRO CATG' with a value of one of:\n"
                  VISIR_UTIL_REPACK_A_ON_PROCATG  " (NodPos: A, ChopPos: on)\n"
                  VISIR_UTIL_REPACK_A_OFF_PROCATG " (NodPos: A, ChopPos: off)\n"
                  VISIR_UTIL_REPACK_B_ON_PROCATG  " (NodPos: B, ChopPos: on)\n"
                  VISIR_UTIL_REPACK_B_OFF_PROCATG " (NodPos: B, ChopPos: off)\n"
                  /* FIXME: reimplement
                  "Additionally, the recipe collapses the on- and off-cubes, "
                  "these product(s) will have a FITS card\n"
                  "'HIERARCH ESO PRO CATG' with a value of one of:\n"
                  VISIR_UTIL_REPACK_MEAN_A_ON_PROCATG
                  " (NodPos: A, ChopPos: on)\n"
                  VISIR_UTIL_REPACK_MEAN_A_OFF_PROCATG
                  " (NodPos: A, ChopPos: off)\n"
                  VISIR_UTIL_REPACK_MEAN_B_ON_PROCATG
                  " (NodPos: B, ChopPos: on)\n"
                  VISIR_UTIL_REPACK_MEAN_B_OFF_PROCATG
                  " (NodPos: B, ChopPos: off)\n" */
                  "For CUBE2, the recipe will produce a static "
                  "bad-pixel map, it will have a FITS card\n"
                  "'HIERARCH ESO PRO CATG' with a value of:\n"
                  VISIR_CALIB_STATIC_MASK"\n"
                  );

/*----------------------------------------------------------------------------*/
/**
 * @defgroup visir_util_repack   Conversion of raw images to nodded images
 */
/*----------------------------------------------------------------------------*/

/*-----------------------------------------------------------------------------
                                Functions code
 -----------------------------------------------------------------------------*/


/*----------------------------------------------------------------------------*/
/**
  @internal
  @brief    pop one image from end of a list
  @param    list       list to pop from
  @return   popped image or null when empty
 */
/*----------------------------------------------------------------------------*/
static cpl_image *
visir_imagelist_pop(cpl_imagelist * list)
{
    if (cpl_imagelist_get_size(list) == 0)
        return NULL;
    return cpl_imagelist_unset(list, cpl_imagelist_get_size(list) - 1);
}


/*----------------------------------------------------------------------------*/
/**
  @internal
  @brief    returns smallest type to save image without loss
  @param    img  the image to be saved
  @return   type

 */
/*----------------------------------------------------------------------------*/
static cpl_type get_optimum_save_type(const cpl_image * img)
{
    cpl_type res = CPL_TYPE_UNSPECIFIED;
    if (cpl_image_get_type(img) == CPL_TYPE_INT) {
        cpl_stats * stats = cpl_stats_new_from_image(img, CPL_STATS_MIN |
                                                     CPL_STATS_MAX);
        if ((int)cpl_stats_get_max(stats) <= CX_MAXSHORT &&
            (int)cpl_stats_get_min(stats) >= CX_MINSHORT)
            res = CPL_TYPE_SHORT;
        cpl_stats_delete(stats);
    }

    return res;
}


/*----------------------------------------------------------------------------*/
/**
  @internal
  @brief    Fill the recipe parameterlist
  @param    self  The parameterlist
  @return   0 iff everything is ok

 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
visir_util_repack_fill_parameterlist(cpl_parameterlist * self)
{
    const char * context = PACKAGE "." RECIPE_STRING;
    cpl_error_code err;

    cpl_ensure_code(self, CPL_ERROR_NULL_INPUT);

    /* Fill the parameters list */

    /* --planestart */
    err = irplib_parameterlist_set_int(self, PACKAGE, RECIPE_STRING, "planestart",
                                       0, NULL, context, "Plane number to "
                                       "start repacking from in each nod cycle,"
                                       "earlier planes are skipped.");
    cpl_ensure_code(!err, err);

    /* --planelimit */
    err = irplib_parameterlist_set_int(self, PACKAGE, RECIPE_STRING, "planelimit",
                       -1, NULL, context, "Limit number of processed input"
                       "planes. It will repack until at least this number "
                       "of input images have been processed or the full "
		       "dataset has been repacked. Always full chop cycles "
		       "need to be repacked so the number is adjusted "
		       "upward to the next multiple of images per chop cycle."
                       "  <= 0 for no limit.");
    cpl_ensure_code(!err, err);

    /* --ncycles */
    err = irplib_parameterlist_set_int(self, PACKAGE, RECIPE_STRING, "ncycles",
                                       -1, NULL, context, "Number of full "
                                       "on-off cycles to repack per nod cycles."
                                       " <= 0 for all.");
    cpl_ensure_code(!err, err);



    /* --trimlow */
    err = irplib_parameterlist_set_int(self, PACKAGE, RECIPE_STRING, "trimlow",
                                       0, NULL, context, "Burst data only. "
                                       "Number of additional planes to cut "
                                       "from before each plane with chopper "
                                       "movement.");
    cpl_ensure_code(!err, err);

    /* --trimhigh */
    err = irplib_parameterlist_set_int(self, PACKAGE, RECIPE_STRING, "trimhigh",
                                       0, NULL, context, "Burst data only. "
                                       "Number of additional planes to cut "
                                       "from after each plane with chopper "
                                       "movement.\n A value of -1 does not "
                                       "skip the plane of the movement.");
    cpl_ensure_code(!err, err);

    /* --bkgcorrect */
    err = irplib_parameterlist_set_string(self, PACKAGE, RECIPE_STRING,
                                          "bkgcorrect", "none", NULL, context,
                                          "Output background corrected planes by "
                                          "subtracting chop (on/off) and nod (A/B) "
                                          "planes\n Options:\n"
                                          " none: no correction\n"
                                          " chop: on - off\n"
                                          " chopnod: (Aon - Aoff) - (Bon - Boff)");
    cpl_ensure_code(!err, err);


    /* --normalize */
    err =
        irplib_parameterlist_set_bool(self, PACKAGE, RECIPE_STRING, "normalize",
                                      CPL_TRUE, NULL, context,
                                      "Normalize planes by DIT");
    cpl_ensure_code(!err, err);


    /* --compress */
    err = irplib_parameterlist_set_bool(self, PACKAGE, RECIPE_STRING,
                                        "compress", CPL_FALSE, NULL, context,
                                        "Apply lossless compression on output"
                                        " files. Can only be done for integer"
                                        " type results.");
    cpl_ensure_code(!err, err);

    /* --lincorrect */
    err = irplib_parameterlist_set_bool(self, PACKAGE, RECIPE_STRING,
                                    "lincorrect", CPL_FALSE, NULL, context,
                                    "Apply linearity correction. Should "
                                    "only be enabled on high flux observations"
                                    ", may degrade results otherwise.");
    cpl_ensure_code(!err, err);

    return CPL_ERROR_NONE;
}


/*----------------------------------------------------------------------------*/
/**
  @internal
  @brief    creates the repack frame state structure
  @param    rawframes   list of rawframes
  @param    iframe      index of the frame looked at
  @param    parlist     list of recipe parameters
  @return   structure with holding the state

  create a convinience structure with all the data needed for repacking.
 */
/*----------------------------------------------------------------------------*/
static repack_framestate *
repack_framestate_new(irplib_framelist * rawframes, int iframe,
                      const cpl_parameterlist * parlist)
{
    repack_framestate * state = cpl_calloc(1, sizeof(repack_framestate));
    int ndit, navrg;
    cpl_frame * frame = irplib_framelist_get(rawframes, iframe);
    const char * filename = cpl_frame_get_filename(frame);
    cpl_size next;
    state->time_max_filewrite = -1;
    state->time_min_obsstart = 1e300;
    cpl_errorstate cleanstate = cpl_errorstate_get();

    state->rawframes = rawframes;

    state->iframe = iframe;

    state->tag = cpl_frame_get_tag(frame);

    error_if(strcmp(state->tag, VISIR_CALIB_STATIC_MASK) == 0,
             CPL_ERROR_UNSUPPORTED_MODE, " ");

    /* compression support disabled, can give bad error messages for
     * INT only acquarius data */
    /* compressed files have the data in extension 1 */
    const cpl_size iext = 0;

    skip_if(irplib_framelist_load_propertylist(rawframes, iframe, iext,
                                               ".*", CPL_FALSE));
    state->parlist = parlist;

    state->plist = irplib_framelist_get_propertylist_const(rawframes, iframe);
    skip_if(0);

    skip_if(visir_get_data_type(frame, state->plist, &state->datatype, &next));

    if (state->datatype == VISIR_DATA_AQU_HCYCLE) {
        /* last frame is INT, but is skipped by when loading */
        state->naxis3 = next;
    }
    else if (state->datatype == VISIR_DATA_AQU_INT) {
        state->naxis3 = 1;
    }
    else if (state->datatype == VISIR_DATA_AQU_BURST_EXT) {
        cpl_propertylist * dplist = cpl_propertylist_load(filename, 1);
        state->naxis3 = visir_pfits_get_naxis3(dplist);
        cpl_propertylist_delete(dplist);
    }
    else
        state->naxis3 = visir_pfits_get_naxis3(state->plist);

    {
        const char * bkgc = irplib_parameterlist_get_string(parlist, PACKAGE,
                                                            RECIPE_STRING,
                                                            "bkgcorrect");
        if (visir_str_par_is_empty(bkgc) || !strcasecmp(bkgc, "none"))
            state->bkgcorrect = VISIR_SUB_NOCORRECT;
        else if (!strcasecmp(bkgc, "chopnod"))
            state->bkgcorrect = VISIR_SUB_CHOPNODCORRECT;
        else if (!strcasecmp(bkgc, "chop"))
            state->bkgcorrect = VISIR_SUB_CHOPCORRECT;
        else
            error_if(1, CPL_ERROR_ILLEGAL_INPUT,
                     "Unknown parameter to --bkgcorrect: %s", bkgc);
    }

    error_if(state->datatype == VISIR_DATA_AQU_INT &&
             state->bkgcorrect != VISIR_SUB_CHOPNODCORRECT,
             CPL_ERROR_INCOMPATIBLE_INPUT,
             "INT frame only data requires --bkgcorrect to be chopnod");

    /* cannot extract a and b hcycle from this format */
    error_if(state->datatype == VISIR_DATA_CUBE1,
             CPL_ERROR_INCOMPATIBLE_INPUT,
             "CUBE1 data not supported please use the legacy recipes "
             "visir_img_combine or visir_spc_obs");

    state->normalize = irplib_parameterlist_get_bool(parlist, PACKAGE,
                                                     RECIPE_STRING,
                                                     "normalize");

    state->dit = visir_pfits_get_dit(state->plist);

    error_if(state->normalize && state->dit == 0, CPL_ERROR_ILLEGAL_INPUT,
             "Cannot normalize by DIT = 0");

    state->planestart = irplib_parameterlist_get_int(parlist, PACKAGE,
                                                     RECIPE_STRING,
                                                     "planestart");
    error_if(state->planestart >= state->naxis3, CPL_ERROR_ILLEGAL_INPUT,
             "planestart %d equal or larger than number of planes %d.",
             state->planestart, state->naxis3);

    ndit = visir_pfits_get_ndit(state->plist);
    navrg = visir_pfits_get_navrg(state->plist);

    IRPLIB_DIAG_PRAGMA_PUSH_ERR(-Wswitch);
    switch(state->datatype) {
        case VISIR_DATA_CUBE1:
            bug_if(1);
            break;
        case VISIR_DATA_AQU_HCYCLE:
        case VISIR_DATA_CUBE2:
            state->nplanes_per_cycle = 2;
            break;
        case VISIR_DATA_AQU_INT:
            /* not actually used */
            state->nplanes_per_cycle = 1;
            break;
        case VISIR_DATA_BURST:
        case VISIR_DATA_AQU_BURST:
        case VISIR_DATA_AQU_BURST_EXT:
            state->nplanes_per_cycle = ndit * 2 / navrg;
            break;
    }
    IRPLIB_DIAG_PRAGMA_POP;

    state->ncycles = irplib_parameterlist_get_int(parlist, PACKAGE,
                                                  RECIPE_STRING, "ncycles");
    if (state->ncycles <= 0)
        state->ncycles = irplib_pfits_get_int(state->plist,
                                       VISIR_PFITS_INT_CHOP_NCYCLES);

    state->planeend = state->planestart + state->ncycles *
                      state->nplanes_per_cycle;
    /* ensure we always repack a multiple of a full cycle to get equal number
     * of on and off planes */
    if (state->planeend >= state->naxis3 || state->planeend <= 0) {
        if (state->planestart % state->nplanes_per_cycle == 0)
            state->planeend = state->naxis3;
        else
            state->planeend = state->naxis3 -
                (state->nplanes_per_cycle -
                 (state->planestart % state->nplanes_per_cycle));
    }

    /* we have max 16 bit adc and cpl int is 32 bit so reasonable dits allow
     * integer normalization without overflow */
    if (state->normalize && (state->dit < 1e-6 ||
        (double)((int)(1. / state->dit)) != 1. / state->dit))
        state->load_type = CPL_TYPE_FLOAT;
    else
        state->load_type = CPL_TYPE_UNSPECIFIED;

    state->trimlow = irplib_parameterlist_get_int(parlist, PACKAGE,
                                                  RECIPE_STRING,
                                                  "trimlow");
    state->trimhigh = irplib_parameterlist_get_int(parlist, PACKAGE,
                                                   RECIPE_STRING,
                                                   "trimhigh");

    state->compress = irplib_parameterlist_get_bool(parlist, PACKAGE,
                                                    RECIPE_STRING,
                                                    "compress");

    if (state->compress && CPL_IO_COMPRESS_RICE == 0) {
        cpl_msg_warning(cpl_func, "CPL version too old for compression.");
        state->compress = CPL_FALSE;
    }

    state->conad = VISIR_UTIL_REPACK_CONAD;

    state->nframes = irplib_framelist_get_size(rawframes);
    state->remregexp  = "^(" VISIR_PFITS_DOUBLE_CUMOFFSETX
                        "|" VISIR_PFITS_DOUBLE_CUMOFFSETY ")$";


    state->pfov = visir_pfits_get_pixscale(state->plist);

    /* Copy comments from CUMOFFSET[XY] */
    state->comoffx      = cpl_propertylist_get_comment
        (state->plist, VISIR_PFITS_DOUBLE_CUMOFFSETX);
    state->comoffy      = cpl_propertylist_get_comment
        (state->plist, VISIR_PFITS_DOUBLE_CUMOFFSETY);
    state->offsetx      = visir_pfits_get_cumoffsetx(state->plist);
    state->offsety      = visir_pfits_get_cumoffsety(state->plist);
    state->jitterwidth  = 0.;
    if (cpl_propertylist_has(state->plist, "ESO SEQ JITTER WIDTH")) {
        state->jitterwidth =
            cpl_propertylist_get_double(state->plist, "ESO SEQ JITTER WIDTH");
        state->jitterwidth /=  state->pfov;
    }

    /* we only have one chip, move PIXSPACE from extension to main header */
    /* TODO still needed? */
    if (next > 0 &&
        !cpl_propertylist_has(state->plist, "ESO DET CHIP1 PXSPACE") &&
        !cpl_propertylist_has(state->plist, "ESO DET CHIP PXSPACE")) {
        cpl_propertylist * extplist = cpl_propertylist_load(filename, 1);
        skip_if(extplist == NULL);
        state->pxspace = visir_pfits_get_pixspace(extplist);
        cpl_propertylist_delete(extplist);
        skip_if(0);
    }

    if (visir_data_is_burst(state->datatype) &&
        visir_data_is_drs(state->datatype)) {
        visir_img_burst_find_delta_chop(state->plist,
                                        &state->to_off,
                                        &state->halfcycle);
    }
    else if (visir_data_is_burst(state->datatype))  {
        error_if(ndit % navrg != 0, CPL_ERROR_ILLEGAL_INPUT,
                 "ndit (%d) not divisible by navrg %d, not supported",
                 ndit, navrg);
        state->halfcycle = ndit / navrg;
    }

    state->bpmthresh = VISIR_HCYCLE_BPM_THRESHOLD;

    if (state->bkgcorrect == VISIR_SUB_NOCORRECT) {
        const char * nodpos     = visir_pfits_get_nodpos(state->plist);
        state->is_a     = nodpos != NULL &&
            (strstr(nodpos, "A") || strstr(nodpos, "a")) ? CPL_TRUE : CPL_FALSE;
        state->procatgon = state->is_a ? VISIR_UTIL_REPACK_A_ON_PROCATG
            : VISIR_UTIL_REPACK_B_ON_PROCATG;
        state->procatgoff = state->is_a ? VISIR_UTIL_REPACK_A_OFF_PROCATG
            : VISIR_UTIL_REPACK_B_OFF_PROCATG;
        state->onname     = cpl_sprintf(RECIPE_STRING "_%s_on_%03d"
                                        CPL_DFS_FITS, nodpos, 1+iframe);
        state->offname    = cpl_sprintf(RECIPE_STRING "_%s_off_%03d"
                                        CPL_DFS_FITS, nodpos, 1+iframe);

        if (!state->is_a) {
            /* Verify that the NODPOS is either A or B */
            skip_if (nodpos == NULL);
            skip_if (strstr(nodpos, "B") == NULL && strstr(nodpos, "b") == NULL);
        }
    } else if (state->bkgcorrect == VISIR_SUB_CHOPCORRECT) {
        const char * nodpos     = visir_pfits_get_nodpos(state->plist);
        state->is_a     = nodpos != NULL &&
            (strstr(nodpos, "A") || strstr(nodpos, "a")) ? CPL_TRUE : CPL_FALSE;
        state->procatgon = state->is_a ? "BKG_CORRECTED_A" : "BKG_CORRECTED_B";
        state->onname    = cpl_sprintf(RECIPE_STRING "_%s_bkgcor_%03d"
                                       CPL_DFS_FITS, nodpos, 1+iframe);
    } else { /*if (state->bkgcorrect == VISIR_SUB_CHOPCORRECT) { */
        const char * nodpos     = visir_pfits_get_nodpos(state->plist);
        state->is_a     = nodpos != NULL &&
            (strstr(nodpos, "A") || strstr(nodpos, "a")) ? CPL_TRUE : CPL_FALSE;
        state->procatgon = VISIR_UTIL_CORRECTED;
        state->onname    = cpl_sprintf(RECIPE_STRING "_bkgcor_%03d"
                                       CPL_DFS_FITS, 1+iframe);
    }

    /* Get the chopping throw in pixels */
    state->pthrow = visir_pfits_get_chop_pthrow(state->plist);
    state->posang = visir_pfits_get_chop_posang(state->plist) -
                    visir_pfits_get_ada_posang(state->plist);

    const char * chopnod_dir = visir_pfits_get_chopnod_dir(state->plist);
    /* can be missing for old spec data */
    if (chopnod_dir == NULL) {
        chopnod_dir = "PARALLEL";
        cpl_errorstate_set(cleanstate);
    }

    /* compute jitter offset */
    if (strcmp(visir_pfits_get_insmode(state->plist), "IMG") == 0) {
        if (strcmp(chopnod_dir, "PARALLEL") == 0) {
            state->posang -= 90 * CPL_MATH_RAD_DEG;
        }
    }
    else if (strncmp(visir_pfits_get_insmode(state->plist), "SPC", 3) == 0) {
        /* spc detector is always parallel and rotated another 90 degree */
        state->posang -= 180 * CPL_MATH_RAD_DEG;
    }
    else {
        cpl_errorstate_set(cleanstate);
        cpl_msg_warning(cpl_func, "Unknown INS MODE %s",
                        visir_pfits_get_insmode(state->plist));
    }
    if (!state->is_a) {
        state->offsetx -= cos(state->posang) * state->pthrow;// - sin(state->posang) * state->pthrow;
        state->offsety += sin(state->posang) * state->pthrow;// + cos(state->posang) * state->pthrow;
    }

    /* spectrograph rotation also swaps -x and y */
    if (visir_data_is_aqu(state->datatype) &&
        strncmp(visir_pfits_get_insmode(state->plist), "SPC", 3) == 0) {
        double tmp = state->offsety;
        state->offsety = -state->offsetx;
        state->offsetx = tmp;
    }


    /* remove rounding errors on no jitter */
    state->offsetx = fabs(state->offsetx) < 0.001 ? 0. : state->offsetx;
    state->offsety = fabs(state->offsety) < 0.001 ? 0. : state->offsety;
    cpl_msg_info(cpl_func, "POSANG: %g; Offset: (%g, %g); Throw: %g; "
                 "Jitterwidth %g", state->posang * CPL_MATH_DEG_RAD, state->offsetx,
                 state->offsety, state->pthrow, state->jitterwidth);

    const char * sresol = visir_pfits_get_resol(state->plist);
    if (sresol && strcmp(sresol, "LRP") == 0) {
        state->resol = VISIR_SPC_R_LRP;
    }
    else if (sresol && strcmp(sresol, "LR") == 0) {
        state->resol = VISIR_SPC_R_LR;
    }
    else if (sresol && strcmp(sresol, "MR") == 0) {
        state->resol = VISIR_SPC_R_MR;
    }
    else if (sresol && strcmp(sresol, "HRS") == 0) {
        state->resol = VISIR_SPC_R_HR;
    }
    else if (sresol && strcmp(sresol, "HRG") == 0) {
        state->resol = VISIR_SPC_R_GHR;
    }
    else {
        state->resol = VISIR_SPC_R_ERR;
    }

    {
        long nx, ny;
        if (state->datatype == VISIR_DATA_AQU_HCYCLE ||
            state->datatype == VISIR_DATA_AQU_INT ||
            state->datatype == VISIR_DATA_AQU_BURST_EXT) {
            cpl_propertylist * plist = cpl_propertylist_load(filename, 1);
            nx = visir_pfits_get_naxis1(plist);
            ny = visir_pfits_get_naxis2(plist);
            state->wcs = cpl_wcs_new_from_propertylist(plist);
            if (state->wcs == NULL) {
                cpl_errorstate_set(cleanstate);
            }
            cpl_propertylist_delete(plist);
        }
        else {
            nx = visir_pfits_get_naxis1(state->plist);
            ny = visir_pfits_get_naxis2(state->plist);
        }
        skip_if(0);

        state->nx = nx;
        state->ny = ny;
    }


    end_skip;

    return state;
}

/* ---------------------------------------------------------------------------*/
/**
 * @brief adapt number of planes to at most maxplanes, accounting for cycles
 *
 * @param state      repack framestate
 * @param maxplanes  max number of planes to reduce, rounded up to full cycles
 */
/* ---------------------------------------------------------------------------*/
static void
repack_framestate_adjust_planeend(repack_framestate * state, int maxplanes)
{
    if (maxplanes <= 0) {
        return;
    }
    int nplanes = state->planeend - state->planestart;
    maxplanes = CX_MAX(maxplanes, state->nplanes_per_cycle);
    if (nplanes - state->nplanes_per_cycle > maxplanes) {
        state->planeend = state->planestart + maxplanes;
        if (maxplanes % state->nplanes_per_cycle != 0) {
            state->planeend += state->nplanes_per_cycle -
                (maxplanes % state->nplanes_per_cycle);
        }
    }
}

/*----------------------------------------------------------------------------*/
/**
  @brief    delete frame state
  @param    state  frame state
 */
/*----------------------------------------------------------------------------*/
static void
repack_framestate_delete(void * state_)
{
    repack_framestate * state = (repack_framestate*) state_;

    if (state == NULL)
        return;

    cpl_wcs_delete(state->wcs);
    cpl_free(state->onname);
    cpl_free(state->offname);
    cpl_free(state);
}


/*----------------------------------------------------------------------------*/
/**
  @brief    store the mean images
  @param    mean      array of images, max size 4
  @param    nmean     array of number of images, max size 4
  @param    frametype array defining which type each entry in rawframes is
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
store_means(const visir_sub_type bkgcorrect, const irplib_framelist * rawfr,
            cpl_frameset * framelist, const cpl_parameterlist * parlist,
            cpl_image ** mean, cpl_size * nmean, visir_frame_type * frametype)
{
    cpl_frameset * usedframes[] = {cpl_frameset_new(), cpl_frameset_new(),
                                   cpl_frameset_new(), cpl_frameset_new()};

    int normalize = irplib_parameterlist_get_bool(parlist, PACKAGE,
                                                  RECIPE_STRING,
                                                  "normalize");
    cpl_propertylist * plist = cpl_propertylist_new();
    if (normalize) {
        cpl_propertylist_append_string(plist, "BUNIT", "adu / s");
    } else {
        cpl_propertylist_append_string(plist, "BUNIT", "adu");
    }

    if (bkgcorrect == VISIR_SUB_CHOPNODCORRECT) {
        for (int i = 1; i < 4; i++) {
            cpl_image_add(mean[0], mean[i]);
            nmean[0] += nmean[i];
        }
        skip_if(cpl_image_divide_scalar(mean[0], nmean[0]));
        for (int i = 0; i < irplib_framelist_get_size(rawfr); i++) {
            cpl_frame * frm =
                cpl_frame_duplicate(irplib_framelist_get_const(rawfr, i));
            cpl_frameset_insert(usedframes[0], frm);
        }
        skip_if(irplib_dfs_save_image(framelist, parlist, usedframes[0],
                                      mean[0], CPL_TYPE_UNSPECIFIED,
                                      RECIPE_STRING, VISIR_UTIL_REPACK_MEAN_PROCATG,
                                      plist, NULL, visir_pipe_id,
                                      "visir_util_repack_mean.fits"));
    }
    else if (bkgcorrect == VISIR_SUB_CHOPCORRECT) {
        const char * procatg[] = {
            VISIR_UTIL_REPACK_MEAN_A_PROCATG,
            VISIR_UTIL_REPACK_MEAN_B_PROCATG};
        const char * fn[] ={"visir_util_repack_mean_A.fits",
                            "visir_util_repack_mean_B.fits"};
        for (int i = 0; i < 2; i++) {
            int idx = i == 0 ? 0 : 2;
            cpl_image_add(mean[idx], mean[idx + 1]);
            nmean[idx] += nmean[idx + 1];
            skip_if(cpl_image_divide_scalar(mean[idx], nmean[idx]));
            for (int j = 0; j < irplib_framelist_get_size(rawfr); j++) {
                if ((i == 0 && frametype[j] == VISIR_FRAME_B) ||
                    (i == 1 && frametype[j] == VISIR_FRAME_A) ||
                    frametype[j] == VISIR_FRAME_STATIC)
                    continue;
                cpl_frame * frm =
                    cpl_frame_duplicate(irplib_framelist_get_const(rawfr, j));
                cpl_frameset_insert(usedframes[i], frm);
            }
            skip_if(irplib_dfs_save_image(framelist, parlist, usedframes[i],
                                          mean[idx], CPL_TYPE_UNSPECIFIED,
                                          RECIPE_STRING, procatg[i],
                                          plist, NULL, visir_pipe_id, fn[i]));
        }
    }
    else {
        const char * procatg[] = {
            VISIR_UTIL_REPACK_MEAN_A_ON_PROCATG,
            VISIR_UTIL_REPACK_MEAN_A_OFF_PROCATG,
            VISIR_UTIL_REPACK_MEAN_B_ON_PROCATG,
            VISIR_UTIL_REPACK_MEAN_B_OFF_PROCATG,
        };
        const char * fn[] ={"visir_util_repack_mean_Aoff.fits",
                            "visir_util_repack_mean_Aon.fits",
                            "visir_util_repack_mean_Boff.fits",
                            "visir_util_repack_mean_Bon.fits"};
        for (int i = 0; i < 4; i++) {
            skip_if(cpl_image_divide_scalar(mean[i], nmean[i]));
            for (int j = 0; j < irplib_framelist_get_size(rawfr); j++) {
                if ((i < 2 && frametype[j] == VISIR_FRAME_A) ||
                    (i >= 2 && frametype[j] == VISIR_FRAME_B) ||
                    frametype[j] == VISIR_FRAME_STATIC)
                    continue;
                cpl_frame * frm =
                    cpl_frame_duplicate(irplib_framelist_get_const(rawfr, j));
                cpl_frameset_insert(usedframes[i], frm);
            }
            skip_if(irplib_dfs_save_image(framelist, parlist, usedframes[i],
                                          mean[i], CPL_TYPE_UNSPECIFIED,
                                          RECIPE_STRING, procatg[i],
                                          plist, NULL, visir_pipe_id, fn[i]));
        }
    }

    end_skip;

    cpl_propertylist_delete(plist);
    for (size_t i = 0; i < sizeof(usedframes)/sizeof(usedframes[0]); i++)
        cpl_frameset_delete(usedframes[i]);

    return cpl_error_get_code();
}


/*----------------------------------------------------------------------------*/
/**
  @brief    The recipe data reduction part is implemented here 
  @param    framelist   the frames list
  @param    parlist     the parameters list
  @return   0 iff everything is ok
 */
/*----------------------------------------------------------------------------*/
static int visir_util_repack(cpl_frameset            * framelist,
                             const cpl_parameterlist * parlist)
{
    irplib_framelist * allframes = NULL;
    irplib_framelist * rawframes = NULL;
    int                n;
    cx_list *          alist = cx_list_new();
    cx_list *          blist = cx_list_new();
    cpl_image *        bpm = NULL;
    /* nonlinear pixel count */
    cpl_image *        nonlinear = NULL;
    cpl_bivector *     lintable = NULL;
    cpl_image * mean[] = {NULL, NULL, NULL, NULL};
    cpl_size nmean[] = {0, 0, 0, 0};
    visir_sub_type bkgcorrect = VISIR_SUB_NOCORRECT;
    visir_frame_type * frametype = NULL;
    int planelimit = 0, nreduced = 0;

    /* Identify the RAW and CALIB frames in the input frameset */
    skip_if (visir_dfs_set_groups(framelist));
    cpl_fits_set_mode(CPL_FITS_START_CACHING);

    /* Objects observation */
    allframes = irplib_framelist_cast(framelist);
    skip_if(allframes == NULL);
    rawframes = irplib_framelist_extract_regexp(allframes, "^("
                                                    VISIR_SPC_OBS_RAW
                                                "|" VISIR_SPC_OBS_ECH_RAW
                                                "|" VISIR_SPC_PHOT_RAW
                                                "|" VISIR_SPC_PHOT_ECH_RAW
                                                "|" VISIR_IMG_PHOT_RAW
                                                "|" VISIR_IMG_COMBINE_CN
                                                "|" VISIR_IMG_COMBINE_CN_BURST
                                                "|" VISIR_IMG_COMBINE_CNJ
                                                "|" VISIR_IMG_COMBINE_CNJ_BURST
                                                "|" VISIR_IMG_COMBINE_CJ
                                                "|" VISIR_IMG_COMBINE_NJ
                                                "|" VISIR_IMG_COMBINE_NJ
                                                "|" VISIR_IMG_CAL_PHOT
                                                "|" VISIR_IMG_CAL_PHOT_BURST
                                                "|" VISIR_IMG_CAL_OBJ
                                                "|" VISIR_IMG_CAL_OBJ_BURST
                                                "|" VISIR_ACQ_CNJ
                                                "|" VISIR_SAM_CAL_N_RAW
                                                "|" VISIR_SAM_CAL_CN_RAW
                                                "|" VISIR_SAM_CAL_NJ_RAW
                                                "|" VISIR_SAM_CAL_CNJ_RAW
                                                "|" VISIR_CORO_CAL_CNJ_RAW
                                                "|" VISIR_SAM_OBS_N_RAW
                                                "|" VISIR_SAM_OBS_CN_RAW
                                                "|" VISIR_SAM_OBS_NJ_RAW
                                                "|" VISIR_SAM_OBS_CNJ_RAW
                                                "|" VISIR_CORO_OBS_CNJ_RAW
                                                "|" VISIR_IMG_BURST
                                                "|" VISIR_UTIL_REPACK_RAW
                                                "|" VISIR_UTIL_INPUTS_RAW
                                                "|" VISIR_UTIL_DATA
                                                ")$",
                                                CPL_FALSE);
    skip_if (rawframes == NULL);

    n = irplib_framelist_get_size(rawframes);
    planelimit = irplib_parameterlist_get_int(parlist, PACKAGE,
                                              RECIPE_STRING, "planelimit");
    frametype = cpl_malloc(sizeof(visir_frame_type) * n);

    for (int i = 0; i < n; i++) {
        repack_framestate * state = NULL;

        state = repack_framestate_new(rawframes, i, parlist);

        frametype[i] = state->is_a ? VISIR_FRAME_A : VISIR_FRAME_B;
        if (state->is_a == CPL_TRUE)
            cx_list_push_back(alist, state);
        else
            cx_list_push_back(blist, state);
        skip_if(0);
    }

    error_if(cx_list_size(alist) == 0, CPL_ERROR_ILLEGAL_INPUT,
             "No frames with nodpos A");
    error_if(cx_list_size(blist) == 0, CPL_ERROR_ILLEGAL_INPUT,
             "No frames with nodpos B");
    if (cx_list_size(alist) != cx_list_size(blist)) {
        if (cx_list_size(alist) > cx_list_size(alist)) {
            cpl_msg_warning(cpl_func,
              "Expecting even number of files, ignoring the last Nod A frame");
            repack_framestate_delete(cx_list_pop_back(alist));
        }
        if (cx_list_size(blist) > cx_list_size(alist)) {
            cpl_msg_warning(cpl_func,
              "Expecting even number of files, ignoring the last Nod B frame");
            repack_framestate_delete(cx_list_pop_back(blist));
        }
    }
    error_if(cx_list_size(alist) != cx_list_size(blist),
             CPL_ERROR_ILLEGAL_INPUT,
             "Unequal number of A and B frames. A: %d, B: %d",
             (int)cx_list_size(alist), (int)cx_list_size(blist));

    {
        cpl_frame * frm = cpl_frameset_find(framelist, VISIR_CALIB_STATIC_MASK);
        repack_framestate * state = ((repack_framestate*)cx_list_front(alist));
        if (frm) {
            bpm = visir_load_bpm(frm, state->datatype,
                                 strncmp(visir_pfits_get_insmode(state->plist),
                                         "SPC", 3) == 0);
            if (bpm == NULL) {
                cpl_msg_warning(cpl_func, "Loading of bad pixel map %s failed",
                                cpl_frame_get_filename(frm));
                skip_if(0);
            }
            if (visir_data_is_aqu(state->datatype)) {
                cpl_size win_nx = visir_pfits_get_win_nx(state->plist);
                cpl_size win_ny = visir_pfits_get_win_ny(state->plist);
                cpl_size start_x = visir_pfits_get_start_x(state->plist);
                cpl_size start_y = visir_pfits_get_start_y(state->plist);
                /* cut bpm to detector readout window */
                cpl_size nx = cpl_image_get_size_x(bpm);
                cpl_size ny = cpl_image_get_size_y(bpm);
                cpl_size llx = 1, lly = 1, urx = nx, ury = ny;
                if (win_nx > 0 && start_x > 0) {
                    llx = start_x;
                    urx = start_x + win_nx - 1;
                }
                if (win_ny > 0 && start_y > 0) {
                    lly = start_y;
                    ury = start_y + win_ny - 1;
                }
                if (llx != 1 || urx != nx || lly != 1 || ury != ny) {
                    cpl_image * tmp = cpl_image_extract(bpm, llx, lly, urx, ury);
                    cpl_image_delete(bpm);
                    bpm = tmp;
                }
                skip_if(0);
            }
            nonlinear = cpl_image_new(cpl_image_get_size_x(bpm),
                                      cpl_image_get_size_y(bpm), CPL_TYPE_INT);
        }
    }

    /* off by default, not accurate enough to work on low count signal data */
    if (irplib_parameterlist_get_bool(parlist, PACKAGE, RECIPE_STRING,
                                      "lincorrect")) {
        repack_framestate * st = cx_list_front(alist);
        cpl_frame * frm = cpl_frameset_find(framelist, VISIR_CALIB_LIN);
        error_if(frm == NULL, CPL_ERROR_INCOMPATIBLE_INPUT,
                 "Linearity correction requested by no input file tagged "
                 "with LINEARITY_TABLE");
        if (visir_data_is_aqu(st->datatype)) {
            if (visir_is_img(st->plist)) {
                lintable = visir_load_lintable(frm, CPL_FALSE);
            }
            else if (visir_is_spc(st->plist)) {
                lintable = visir_load_lintable(frm, CPL_TRUE);
            }
        }
    }

    while (1) {
        repack_framestate * fstatea = NULL, * fstateb = NULL;
        if (!cx_list_empty(alist))
            fstatea = (repack_framestate*)cx_list_pop_front(alist);
        if (!cx_list_empty(blist))
            fstateb = (repack_framestate*)cx_list_pop_front(blist);

        if (fstatea == NULL || fstateb == NULL)
            break;

        if (lintable) {
            fstatea->load_type = CPL_TYPE_FLOAT;
            fstateb->load_type = CPL_TYPE_FLOAT;
        }


        if (cpl_propertylist_has(fstatea->plist, "DATE-OBS") &&
            cpl_propertylist_has(fstatea->plist, "DATE") &&
            cpl_propertylist_has(fstateb->plist, "DATE-OBS") &&
            cpl_propertylist_has(fstateb->plist, "DATE")) {
            const char * str =
                cpl_propertylist_get_string(fstatea->plist, "DATE-OBS");
            irplib_wcs_mjd_from_string(&(fstatea->time_obsstart), str);
            str = cpl_propertylist_get_string(fstatea->plist, "DATE");
            irplib_wcs_mjd_from_string(&(fstatea->time_filewrite), str);

            str = cpl_propertylist_get_string(fstateb->plist, "DATE-OBS");
            irplib_wcs_mjd_from_string(&(fstateb->time_obsstart), str);
            str = cpl_propertylist_get_string(fstateb->plist, "DATE");
            irplib_wcs_mjd_from_string(&(fstateb->time_filewrite), str);
        }

        fstatea->time_min_obsstart = CX_MIN(fstatea->time_obsstart,
                                            fstateb->time_obsstart);
        fstateb->time_min_obsstart = CX_MIN(fstatea->time_obsstart,
                                            fstateb->time_obsstart);
        fstatea->time_max_filewrite = CX_MAX(fstatea->time_filewrite,
                                             fstateb->time_filewrite);
        fstateb->time_max_filewrite = CX_MAX(fstatea->time_filewrite,
                                             fstateb->time_filewrite);

        for (int i = 0; i < 4; i++) {
            if (mean[i] == NULL) {
                mean[i] = cpl_image_new(fstatea->nx, fstatea->ny, CPL_TYPE_DOUBLE);
            }
        }
        skip_if(0);
        /* order is important, see store_means
         * updates are currently single threaded, if not add locks or allocate
         * on demand as there can be many nod pairs */
        fstatea->mean_on = mean[0];
        fstatea->mean_off = mean[1];
        fstateb->mean_on = mean[2];
        fstateb->mean_off = mean[3];
        fstatea->nmean_on = &nmean[0];
        fstatea->nmean_off = &nmean[1];
        fstateb->nmean_on = &nmean[2];
        fstateb->nmean_off = &nmean[3];

        repack_framestate_adjust_planeend(fstatea, planelimit - nreduced);
        repack_framestate_adjust_planeend(fstateb, planelimit - nreduced);

        /* run single threaded but allow two async reader tasks */
        OMP_PRAGMA(omp parallel num_threads(3))
        OMP_PRAGMA(omp single)
        {
            visir_util_repack_two(fstatea, fstateb, framelist, &bpm, nonlinear,
                                  lintable);
        }
        skip_if(0);

        bkgcorrect = fstatea->bkgcorrect;

        nreduced += fstatea->planeend - fstatea->planestart;

        repack_framestate_delete(fstatea);
        repack_framestate_delete(fstateb);
        cpl_fits_set_mode(CPL_FITS_RESTART_CACHING);
        if (planelimit > 0 && nreduced >= planelimit)
            break;
    }

    skip_if(store_means(bkgcorrect, rawframes, framelist, parlist,
                        mean, nmean, frametype));

    /* store bpm, skip if wrong size, there is no keyword one can
     * use in the OCA rules to associate DRS/AQU properly */
    if (bpm &&
        cpl_image_get_size_x(bpm) == cpl_image_get_size_x(mean[0]) &&
        cpl_image_get_size_y(bpm) == cpl_image_get_size_y(mean[0])) {
        /* apply all-nonlinear bpm */
        if (lintable) {
            cpl_size total = nmean[0] + nmean[1] + nmean[2] + nmean[3];
            cpl_mask * mnonlinear =
                cpl_mask_threshold_image_create(nonlinear, total * 0.9, total * 2);
            cpl_msg_info(cpl_func, "%lld (%.3g%%) pixels non-linear in 90%% "
                         "of %lld frames", cpl_mask_count(mnonlinear),
                         (100. * cpl_mask_count(mnonlinear)) /
                         (cpl_mask_get_size_x(mnonlinear) *
                          cpl_mask_get_size_y(mnonlinear)), total);
            /* disabled due to problems in HRS with so many bad pixels */
            //cpl_image_reject_from_mask(bpm, mnonlinear);
            //cpl_image_fill_rejected(bpm, 1);
            cpl_mask_delete(mnonlinear);
        }

        cpl_frameset  * usedframes = cpl_frameset_new();
        cpl_frameset_insert(usedframes, cpl_frame_duplicate
                            (irplib_framelist_get_const(rawframes, 0)));
        irplib_dfs_save_image(framelist, parlist, usedframes, bpm,
                              CPL_BPP_8_UNSIGNED, RECIPE_STRING,
                              VISIR_CALIB_STATIC_MASK, NULL, NULL,
                              visir_pipe_id, RECIPE_STRING "_bpm"
                              CPL_DFS_FITS);
        cpl_frameset_delete(usedframes);
    }
    else if (bpm) {
        cpl_msg_warning(cpl_func, "Provided bad pixel mask does not have "
                        "correct size, skipping");
    }

    end_skip;

    irplib_framelist_delete(allframes);
    irplib_framelist_delete(rawframes);

    cx_list_destroy(alist, repack_framestate_delete);
    cx_list_destroy(blist, repack_framestate_delete);
    cpl_image_delete(bpm);
    cpl_image_delete(nonlinear);
    cpl_bivector_delete(lintable);
    cpl_free(frametype);

    for (size_t i = 0; i < sizeof(mean)/sizeof(mean[0]); i++)
        cpl_image_delete(mean[i]);

    return cpl_error_get_code();
}


/*----------------------------------------------------------------------------*/
/**
  @internal
  @brief    append subtracted images to a file
  @param    state       repack state structure
  @param    subtracted  imagelist to save
  @param    jit_cor     jitter corrected imagelist for the mean or NULL
  @param    plist       propertylist to save with the image
  @return   CPL_ERROR_NONE iff OK, otherwise the relevant CPL error code.

  Will append images to an existing file.
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
save_subtract_images(const repack_framestate * state,
                     const cpl_imagelist * subtracted,
                     const cpl_imagelist * jit_cor,
                     const cpl_propertylist * plist)
{
    const cpl_size nsub = cpl_imagelist_get_size(subtracted);
    cpl_ensure_code(jit_cor == NULL || cpl_imagelist_get_size(jit_cor) == nsub,
                    CPL_ERROR_ILLEGAL_INPUT);


    for (cpl_size j = 0; j < nsub; j++) {
        const cpl_image * img = cpl_imagelist_get_const(subtracted, j);
        cpl_io_type compress = 0;
        cpl_type save_type;

        if (jit_cor)
            cpl_image_add(state->mean_on, cpl_imagelist_get_const(jit_cor, j));
        else
            cpl_image_add(state->mean_on, img);
        (*state->nmean_on)++;

        if (state->compress && cpl_image_get_type(img) == CPL_TYPE_INT) {
            /* saves space even if it could be stored in a short */
            save_type = CPL_TYPE_INT;
            compress = CPL_IO_COMPRESS_RICE;
        }
        else
            save_type = get_optimum_save_type(img);

        skip_if(cpl_image_save(img, state->onname, save_type,
                               plist, CPL_IO_EXTEND | compress));
    }

    end_skip;

    return cpl_error_get_code();
}


/*----------------------------------------------------------------------------*/
/**
  @internal
  @brief    append images to a file
  @param    state       repack state structure
  @param    images      imagelist to save
  @param    plist       propertylist to save with the image
  @param    on          whether its an on or off image
  @return   CPL_ERROR_NONE iff OK, otherwise the relevant CPL error code.

  Will append images to an existing file.
  In case of burst data it will flip the sign.
 */
/*----------------------------------------------------------------------------*/
static
cpl_error_code append_images(const repack_framestate * state,
                             cpl_imagelist * images,
                             const cpl_propertylist * plist,
                             const cpl_boolean on)
{
    const char * name = on ? state->onname : state->offname;
    const cpl_size n = cpl_imagelist_get_size(images);
    cpl_image * mimg = on ? state->mean_on : state->mean_off;
    cpl_size * nmean = on ? state->nmean_on : state->nmean_off;

    for (cpl_size j = 0; j < n; j++) {
        cpl_image * img  = cpl_imagelist_get(images,  j);
        cpl_io_type compress = 0;
        cpl_type save_type;
        /* drs burst data is not preprocessed so it needs flipping */
        if (state->datatype == VISIR_DATA_BURST) {
            cpl_image_multiply_scalar(img, -1);
            cpl_image_add_scalar(img, VISIR_HCYCLE_OFFSET);
        }

        cpl_image_add(mimg, img);
        (*nmean)++;

        if (state->compress && cpl_image_get_type(img) == CPL_TYPE_INT) {
            /* saves space even if it could be stored in a short */
            save_type = CPL_TYPE_INT;
            compress = CPL_IO_COMPRESS_RICE;
        }
        else
            save_type = get_optimum_save_type(img);

        skip_if(cpl_image_save(img,  name, save_type,
                               plist, CPL_IO_EXTEND | compress));
    }

    end_skip;

    return cpl_error_get_code();
}


/*----------------------------------------------------------------------------*/
/**
  @internal
  @brief    cast imagelist to type
  @param    l     imagelist
  @param    type  image type
  @return   CPL_ERROR_NONE iff OK, otherwise the relevant CPL error code.
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
cast_list(cpl_imagelist * l, const cpl_type type)
{
    const cpl_size n = cpl_imagelist_get_size(l);
    cpl_imagelist * cl;
    if (type == CPL_TYPE_UNSPECIFIED)
        return CPL_ERROR_NONE;

    cl = cpl_imagelist_new();

    for (cpl_size i = 0; i < n; i++) {
        cpl_image * img = cpl_imagelist_get(l, i);
        cpl_image * cast;
        if (cpl_image_get_type(img) == type) {
            cpl_imagelist_delete(cl);
            return cpl_error_get_code();
        }
        cast = cpl_image_cast(img, type);
        cpl_imagelist_set(cl, cast, i);
    }

    cpl_imagelist_empty(l);
    for (cpl_size i = 0; i < n; i++)
        cpl_imagelist_set(l, cpl_imagelist_get(cl, i), i);

    visir_imagelist_unwrap(cl);

    return cpl_error_get_code();
}


/*----------------------------------------------------------------------------*/
/**
  @internal
  @brief    load a chunk of data
  @param    on          imagelist to save chop on data
  @param    off         imagelist to save chop off data
  @param    state       repack frame state
  @param    pstart      plane to start loading from
  @param    pend        plane end, not loaded
  @return   CPL_ERROR_NONE iff OK, otherwise the relevant CPL error code.
 */
/*----------------------------------------------------------------------------*/
static
cpl_error_code load_chunk(cpl_imagelist * on, cpl_imagelist * off,
                          const repack_framestate * state,
                          const int pstart, const int pend)
{
    /* sort a chunk of data */
    if (state->datatype == VISIR_DATA_AQU_HCYCLE) {
        const cpl_frame * frame = irplib_framelist_get_const(state->rawframes,
                                                             state->iframe);
        const char * fn = cpl_frame_get_filename(frame);
        skip_if(0);

        for (cpl_size iext = pstart + 1; iext < pend + 1; iext++) {
            const char * ftype;
            cpl_propertylist * plist =
                cpl_propertylist_load_regexp(fn, iext,
                                             VISIR_PFITS_STRING_FRAME_TYPE,
                                             CPL_FALSE);
            skip_if(plist == NULL);
            ftype = visir_pfits_get_frame_type(plist);
            error_if(ftype == NULL, CPL_ERROR_DATA_NOT_FOUND, "ESO DET FRAM "
                     "TYPE keyword missing in extension %d of file %s",
                     (int)iext, fn);
            if (strcmp(ftype, "HCYCLE1") == 0)
                cpl_imagelist_set(on,
                                  cpl_image_load(fn, CPL_TYPE_FLOAT, 0, iext),
                                  cpl_imagelist_get_size(on));
            else if (strcmp(ftype, "HCYCLE2") == 0)
                cpl_imagelist_set(off,
                                  cpl_image_load(fn, CPL_TYPE_FLOAT, 0, iext),
                                  cpl_imagelist_get_size(off));
            else {
                cpl_msg_debug(cpl_func, "Skipping \"%s\" frame type", ftype);
            }
            cpl_propertylist_delete(plist);
            skip_if(0);
        }
    }
    else if (state->datatype == VISIR_DATA_CUBE2) {
        skip_if(visir_load_cube2_split(on, off, state->rawframes,
                                       state->iframe, pstart, pend));

    }
    else if (visir_data_is_burst(state->datatype)) {
        const cpl_frame * frame = irplib_framelist_get_const(state->rawframes,
                                                             state->iframe);
        /* TODO rework loading from first extension */
        if (state->datatype == VISIR_DATA_AQU_BURST_EXT)  {
            cpl_propertylist * plist =
                cpl_propertylist_load(cpl_frame_get_filename(frame), 1);
            /* hack to get it to load from 1. extension */
            cpl_propertylist_update_int(plist, "ZNAXIS3", visir_pfits_get_naxis3(plist));
            skip_if(visir_load_burst_aqu(on, off, frame, plist,
                                         state->halfcycle , pstart, pend));
            cpl_propertylist_delete(plist);
        }
        else {
            skip_if(visir_load_burst(on, off, frame,
                                     state->plist, state->to_off,
                                     state->halfcycle, pstart, pend,
                                     state->trimlow, state->trimhigh));
        }
    }
    else
        error_if(1, CPL_ERROR_ILLEGAL_INPUT, "invalid data tag");

    /* FIXME: pass load type to the loading functions instead */
    cast_list(on, state->load_type);
    cast_list(off, state->load_type);

    end_skip;

    return cpl_error_get_code();
}


/*----------------------------------------------------------------------------*/
/**
  @internal
  @brief    equalize length of lists
  @param    a           list a
  @param    b           list b
  @return   size of the equalized lists or -1 on error

  pops difference of #a/#b of the longer list
 */
/*----------------------------------------------------------------------------*/
static cpl_size
equalize_lists(cpl_imagelist * a, cpl_imagelist * b)
{
    const cpl_size na = cpl_imagelist_get_size(a);
    const cpl_size nb = cpl_imagelist_get_size(b);

    if (na != nb)
        cpl_msg_warning(cpl_func, "Unequal number of planes in on "
                        "and off list: #on %d, #off %d. Skipping %ld"
                        " planes.", (int)na, (int)nb, labs(na - nb));

    if (na > nb) {
        for (cpl_size i = 0; i < na - nb; i++)
            cpl_image_delete(visir_imagelist_pop(a));
    }
    else if (na < nb) {
        for (cpl_size i = 0; i < nb - na; i++)
            cpl_image_delete(visir_imagelist_pop(b));
    }

    if (cpl_error_get_code() != CPL_ERROR_NONE)
        return -1;
    return cpl_imagelist_get_size(a);
}

static cpl_error_code
check_and_fix_cd_wcs(const repack_framestate * state, cpl_propertylist * plist)
{
    cpl_errorstate cleanstate = cpl_errorstate_get();
    /* move wcs from extension to primary as needed by detect_shift
     * but only if primary does not already have wcs as then the ext is wrong
     * (aqu data < 2016.02) */
    if (state->wcs && !cpl_propertylist_has(plist, "CRVAL1")) {
        const cpl_matrix * m;
        const cpl_array * v = cpl_wcs_get_crval(state->wcs);
        cpl_propertylist_update_double(plist, "CRVAL1",
                                       cpl_array_get_double(v, 0, NULL));
        cpl_propertylist_update_double(plist, "CRVAL2",
                                       cpl_array_get_double(v, 1, NULL));
        v = cpl_wcs_get_ctype(state->wcs);
        cpl_propertylist_update_string(plist, "CTYPE1",
                                       cpl_array_get_string(v, 0));
        cpl_propertylist_update_string(plist, "CTYPE2",
                                       cpl_array_get_string(v, 1));
        v = cpl_wcs_get_crpix(state->wcs);
        cpl_propertylist_update_double(plist, "CRPIX1",
                                       cpl_array_get_double(v, 0, NULL));
        cpl_propertylist_update_double(plist, "CRPIX2",
                                       cpl_array_get_double(v, 1, NULL));
        m = cpl_wcs_get_cd(state->wcs);
        cpl_propertylist_update_double(plist, "CD1_1",
                                       cpl_matrix_get(m, 0, 0));
        cpl_propertylist_append_double(plist, "CD1_2",
                                       cpl_matrix_get(m, 0, 1));
        cpl_propertylist_update_double(plist, "CD2_1",
                                       cpl_matrix_get(m, 1, 0));
        cpl_propertylist_update_double(plist, "CD2_2",
                                       cpl_matrix_get(m, 1, 1));
        v = cpl_wcs_get_cunit(state->wcs);
        cpl_propertylist_update_string(plist, "CUNIT1",
                                       cpl_array_get_string(v, 0));
        cpl_propertylist_update_string(plist, "CUNIT2",
                                       cpl_array_get_string(v, 1));
    }
    cpl_boolean invalid =
        (cpl_propertylist_get_double(plist, "CD1_1") == 0. &&
         cpl_propertylist_get_double(plist, "CD1_2") == 0.) ||
        (cpl_propertylist_get_double(plist, "CD2_1") == 0. &&
         cpl_propertylist_get_double(plist, "CD2_2") == 0.);
    invalid = invalid || (cpl_error_get_code() != CPL_ERROR_NONE);
    cpl_errorstate_set(cleanstate);

    if (invalid) {
        double pfov = visir_pfits_get_pixscale(state->plist);
        cpl_msg_warning(cpl_func, "CDX_Y WCS key missing or zero, fixing to "
                        "%g (pfov) / 3600", pfov);
        cpl_propertylist_update_double(plist, "CD1_1", -pfov / 3600.);
        cpl_propertylist_update_double(plist, "CD1_2", 0.);
        cpl_propertylist_update_double(plist, "CD2_1", 0.);
        cpl_propertylist_update_double(plist, "CD2_2", pfov / 3600.);
    }

    return cpl_error_get_code();
}


/*----------------------------------------------------------------------------*/
/**
  @internal
  @brief    prepare output fits files
  @param    state       repack state
  @param    products    products frameset
  @param    xjittera    jitter of frame
  @param    yjittera    jitter of frame
  @param    xjitterb    jitter of frame
  @param    yjitterb    jitter of frame
  @param    nodtype     type of nod (ANOD, BNOD, ABNOD)
  @return   CPL_ERROR_NONE iff OK, otherwise the relevant CPL error code.

 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
prepare_output(const repack_framestate * state, cpl_frameset * products,
               double bg_mean,
               enum nodtype_e nodtype)
{
    cpl_frameset  * usedframes = cpl_frameset_new();
    cpl_propertylist * onlist  = cpl_propertylist_new();
    cpl_propertylist * offlist = cpl_propertylist_new();

    /* Need a copy of the WCS cards for subsequent beam stacking */
    bug_if(cpl_propertylist_copy_property_regexp(onlist, state->plist, "^("
                                                 IRPLIB_PFITS_WCS_REGEXP ")$",
                                                 CPL_FALSE));

    bug_if(cpl_propertylist_append_double(onlist, VISIR_DRS_CONAD, state->conad));
    bug_if(cpl_propertylist_set_comment(onlist, VISIR_DRS_CONAD, "Default "
                                        "single frame value: " IRPLIB_STRINGIFY
                                        (VISIR_UTIL_REPACK_CONAD)));

    bug_if(cpl_propertylist_append_double(onlist, VISIR_PFITS_DOUBLE_PIXSPACE,
                                          state->pxspace));

    if (nodtype == ANOD || nodtype == ABNOD) {
        const char * keyx = nodtype == ABNOD ?
            VISIR_DRS_CUMOFFSETXA : VISIR_DRS_CUMOFFSETX;
        const char * keyy = nodtype == ABNOD ?
            VISIR_DRS_CUMOFFSETYA : VISIR_DRS_CUMOFFSETY;
        cpl_propertylist_append_double(onlist, keyx, state->offsetx);
        cpl_propertylist_append_double(onlist, keyy, state->offsety);
        cpl_propertylist_set_comment(onlist, keyx, state->comoffx);
        cpl_propertylist_set_comment(onlist, keyy, state->comoffy);
    }

    if (nodtype == BNOD || nodtype == ABNOD) {
        const char * keyx = nodtype == ABNOD ?
            VISIR_DRS_CUMOFFSETXB : VISIR_DRS_CUMOFFSETX;
        const char * keyy = nodtype == ABNOD ?
            VISIR_DRS_CUMOFFSETYB : VISIR_DRS_CUMOFFSETY;
        cpl_propertylist_append_double(onlist, keyx, state->offsetx);
        cpl_propertylist_append_double(onlist, keyy, state->offsety);
        cpl_propertylist_set_comment(onlist, keyx, state->comoffx);
        cpl_propertylist_set_comment(onlist, keyy, state->comoffy);
    }

    bug_if(cpl_propertylist_append(offlist, onlist));

    cpl_propertylist_append_int(onlist, "ESO DRS DTYPE", state->datatype);
    cpl_propertylist_append_int(offlist, "ESO DRS DTYPE", state->datatype);
    cpl_propertylist_append_string(offlist, "ESO DRS CATG", state->tag);
    cpl_propertylist_append_string(onlist, "ESO DRS CATG", state->tag);
    bug_if(0);

    if (state->time_max_filewrite > 0) {
        cpl_propertylist_append_double(onlist, "ESO DRS DATE",
                                       state->time_max_filewrite);
        cpl_propertylist_append_double(offlist, "ESO DRS DATE",
                                       state->time_max_filewrite);
        cpl_propertylist_append_double(onlist, "ESO DRS DATE-OBS",
                                       state->time_min_obsstart);
        cpl_propertylist_append_double(offlist, "ESO DRS DATE-OBS",
                                       state->time_min_obsstart);
    }
    else {
        /* always write so following recipes don't need to check */
        cpl_propertylist_append_double(onlist, "ESO DRS DATE", -1.);
        cpl_propertylist_append_double(offlist, "ESO DRS DATE", -1.);
        cpl_propertylist_append_double(onlist, "ESO DRS DATE-OBS", -1.);
        cpl_propertylist_append_double(offlist, "ESO DRS DATE-OBS", -1.);
    }
    if (state->normalize) {
        cpl_propertylist_append_string(onlist, "BUNIT", "adu / s");
        cpl_propertylist_append_string(offlist, "BUNIT", "adu / s");
    }
    else {
        cpl_propertylist_append_string(onlist, "BUNIT", "adu");
        cpl_propertylist_append_string(offlist, "BUNIT", "adu");
    }

    /* record all inputs as used, even though not all are actually used for
     * each output but not recording them here would then require merging input
     * headers in subsequent recipes which do join the output files into a
     * single product */
    for (int i = 0; i < state->nframes; i++) {
        const cpl_frame * frm =
            irplib_framelist_get_const(state->rawframes, i);
        bug_if(cpl_frameset_insert(usedframes, cpl_frame_duplicate(frm)));
    }

    skip_if(check_and_fix_cd_wcs(state, onlist));
    skip_if(check_and_fix_cd_wcs(state, offlist));

    if (products == NULL)
        products = cpl_frameset_new();


    if (state->datatype == VISIR_DATA_CUBE2) {
        skip_if(visir_qc_append_background(onlist, state->rawframes, 0, 0));
        skip_if(visir_qc_append_background(offlist, state->rawframes, 0, 0));
    }
    else if (!isnan(bg_mean)) {
        bug_if (cpl_propertylist_append_double(onlist, "ESO QC BACKGD MEAN",
                                               bg_mean));
        bug_if (cpl_propertylist_append_double(offlist, "ESO QC BACKGD MEAN",
                                               bg_mean));
    }

    if (state->bkgcorrect == VISIR_SUB_CHOPCORRECT ||
        state->bkgcorrect == VISIR_SUB_CHOPNODCORRECT) {
        skip_if(irplib_dfs_save_propertylist(products, state->parlist, usedframes,
                                             RECIPE_STRING, state->procatgon,
                                             onlist, state->remregexp,
                                             visir_pipe_id,
                                             state->onname));
    } else {
        /* Save each image in a separate extension */
        /* Do not save data in the primary data unit (DFS10475) */
        skip_if(irplib_dfs_save_propertylist(products, state->parlist, usedframes,
                                             RECIPE_STRING, state->procatgon,
                                             onlist, state->remregexp,
                                             visir_pipe_id, state->onname));
        skip_if(irplib_dfs_save_propertylist(products, state->parlist, usedframes,
                                             RECIPE_STRING, state->procatgoff,
                                             offlist, state->remregexp,
                                             visir_pipe_id, state->offname));
    }

    end_skip;
    cpl_frameset_delete(usedframes);
    cpl_propertylist_delete(onlist);
    cpl_propertylist_delete(offlist);

    return cpl_error_get_code();
}


static cpl_imagelist *
correct_jitter(const cpl_imagelist * imgs, double dx, double dy)
{
    cpl_imagelist * jit_cor = cpl_imagelist_new();
    cpl_msg_info(cpl_func, "Correcting jitter, x: %g y: %g", dx, dy);
    for (cpl_size i = 0, n = cpl_imagelist_get_size(imgs); i < n; i++) {
        cpl_image * img = cpl_image_duplicate(cpl_imagelist_get_const(imgs, i));
        /* only used for mean which in turn is only used for
         * initial beam guess, pixel precision sufficient */
        skip_if(cpl_image_shift(img,
                                -visir_round_to_int(dx),
                                -visir_round_to_int(dy)));
        cpl_imagelist_set(jit_cor, img, i);
    }

    end_skip;

    return jit_cor;
}


/* ---------------------------------------------------------------------------*/
/**
 * @brief chop correct on and off imagelist inplace
 *
 * @param state       repack state
 * @param on          on list
 * @param off         off list
 * @param last_chunk  whether processing last chunk
 *
 * @return chop corrected imagelist
 *         it is one of the input lists, the other is emptied
 *
 * if last chunk equalizes lists
 * subtracts on from off (handling sign flipped burst data)
 * normalizes by dit if required
 */
/* ---------------------------------------------------------------------------*/
static cpl_imagelist *
chop_correct(const repack_framestate * state,
             cpl_imagelist * on,
             cpl_imagelist * off,
             const cpl_boolean last_chunk)
{
    cpl_imagelist * sub;
    /* burst data is not preprocessed and needs sign flipping */
    cpl_imagelist * subt;

    subt = state->datatype == VISIR_DATA_BURST ? on : off;
    sub  = state->datatype == VISIR_DATA_BURST ? off : on;

    if (last_chunk)
        equalize_lists(on, off);

    cpl_imagelist_subtract(sub, subt);
    if (state->normalize) {
        cpl_imagelist_multiply_scalar(sub, 1. / (state->dit * 2));
    }
    /* empty now to reduce memory usage */
    cpl_imagelist_empty(subt);

    return sub;
}

/* ---------------------------------------------------------------------------*/
/**
 * @brief compute sum of median of each image
 *
 * @param list    imagelist
 * @param llx     lower left x window (FITS)
 * @param lly     lower left y window (FITS)
 * @param urx     upper right x window (FITS)
 * @param ury     upper right y window (FITS)
 * @param nimages number of images summed added to this variable
 */
/* ---------------------------------------------------------------------------*/
static double
compute_qc_median(const cpl_imagelist * list, cpl_size * nimages,
                  cpl_size llx, cpl_size lly, cpl_size urx, cpl_size ury)
{
    double sum = 0.;
    for (cpl_size i = 0; i < cpl_imagelist_get_size(list); i++) {
        const cpl_image * img = cpl_imagelist_get_const(list, i);
        llx = CPL_MIN(cpl_image_get_size_x(img), llx);
        lly = CPL_MIN(cpl_image_get_size_y(img), lly);
        urx = CPL_MIN(cpl_image_get_size_x(img), urx);
        ury = CPL_MIN(cpl_image_get_size_y(img), ury);
        sum += cpl_image_get_median_window(img, llx, lly, urx, ury);
    }

    *nimages += cpl_imagelist_get_size(list);

    return sum;
}

/* ---------------------------------------------------------------------------*/
/**
 * @brief correct linearity based on input table
 *
 * @param state    repack state
 * @param images   images to correct
 * @param lintable
 */
/* ---------------------------------------------------------------------------*/
static cpl_error_code
correct_linearity(const repack_framestate * state,
                  cpl_imagelist * images,
                  const cpl_bivector * lintable, cpl_image * nonlinear)
{
    if (lintable == NULL || !visir_data_is_aqu(state->datatype)) {
        return CPL_ERROR_NONE;
    }
    for (size_t i = 0; i < (size_t)cpl_imagelist_get_size(images); i++) {
        cpl_image * img = cpl_imagelist_get(images, i);
        cpl_ensure_code(cpl_image_get_type(img) == CPL_TYPE_FLOAT ||
                        cpl_image_get_type(img) == CPL_TYPE_DOUBLE,
                        CPL_ERROR_ILLEGAL_INPUT);
        cpl_image * dvals = cpl_image_cast(img, CPL_TYPE_DOUBLE);
        cpl_image * corvals = visir_linintp_values(dvals, lintable);
        if (nonlinear) {
            cpl_image * bad =
                cpl_image_new_from_mask(cpl_image_get_bpm(corvals));
            cpl_image_add(nonlinear, bad);
            cpl_image_delete(bad);
        }
        cpl_image_divide(img, corvals);
        cpl_image_delete(dvals);
        cpl_image_delete(corvals);
    }

    return cpl_error_get_code();
}

/* ---------------------------------------------------------------------------*/
/**
 * @brief chop-nod correct and save data
 *
 * @param fstatea repack state of nod a
 * @param fstateb repack state of nod b
 * @param aon     nod a on list  empty after return
 * @param aoff    nod a off list empty after return
 * @param bon     nod b on list  empty after return
 * @param boff    nod b off list empty after return
 * @param last_chunk  whether processing last chunk
 */
/* ---------------------------------------------------------------------------*/
static cpl_error_code
save_corrected(const repack_framestate * fstatea,
               const repack_framestate * fstateb,
               cpl_imagelist * aon,
               cpl_imagelist * aoff,
               cpl_imagelist * bon,
               cpl_imagelist * boff,
               const cpl_boolean last_chunk)
{
    cpl_imagelist * chop_corrected_a, * chop_corrected_b;
    const repack_framestate * state = fstatea;

    chop_corrected_a = chop_correct(fstatea, aon, aoff, last_chunk);
    chop_corrected_b = chop_correct(fstateb, bon, boff, last_chunk);

    if (cpl_error_get_code())
        return cpl_error_get_code();

    if (state->bkgcorrect == VISIR_SUB_CHOPCORRECT) {
        /* only chop correction requested */
        save_subtract_images(fstatea, chop_corrected_a, NULL, NULL);
        save_subtract_images(fstateb, chop_corrected_b, NULL, NULL);
    }
    else {
        /* perform nod and jitter correction
         * jitter is only used for the mean used for object detection pivoting
         * correction is done in later recipes */
        cpl_imagelist * jca = NULL;
        cpl_imagelist * jcb = NULL;

        if (last_chunk)
            equalize_lists(chop_corrected_a, chop_corrected_b);

        if (state->normalize) {
            cpl_imagelist_divide_scalar(chop_corrected_a, 2);
            cpl_imagelist_divide_scalar(chop_corrected_b, 2);
        }

        if (visir_round_to_int(fstatea->offsetx) != 0 ||
            visir_round_to_int(fstatea->offsety) != 0 ||
            visir_round_to_int(fstateb->offsetx) != 0 ||
            visir_round_to_int(fstateb->offsety) != 0) {
            jca = correct_jitter(chop_corrected_a, fstatea->offsetx, fstatea->offsety);
            jcb = correct_jitter(chop_corrected_b, fstateb->offsetx, fstateb->offsety);
            cpl_imagelist_subtract(jca, jcb);
        }

        cpl_imagelist_subtract(chop_corrected_a, chop_corrected_b);

        save_subtract_images(state, chop_corrected_a, jca, NULL);
        cpl_imagelist_delete(jcb);
        cpl_imagelist_delete(jca);
    }

    return cpl_error_get_code();
}

static cpl_error_code
save_uncorrected(const repack_framestate * fstatea,
                 const repack_framestate * fstateb,
                 cpl_imagelist * aon,
                 cpl_imagelist * aoff,
                 cpl_imagelist * bon,
                 cpl_imagelist * boff)
{
    const repack_framestate * state = fstatea;
    if (state->normalize) {
        cpl_imagelist_multiply_scalar(aon,  1. / fstatea->dit);
        cpl_imagelist_multiply_scalar(aoff, 1. / fstatea->dit);
        cpl_imagelist_multiply_scalar(bon,  1. / fstateb->dit);
        cpl_imagelist_multiply_scalar(boff, 1. / fstateb->dit);
    }
    append_images(fstatea, aon, NULL, CPL_TRUE);
    append_images(fstatea, aoff, NULL, CPL_FALSE);
    append_images(fstateb, bon, NULL, CPL_TRUE);
    append_images(fstateb, boff, NULL, CPL_FALSE);
    cpl_imagelist_empty(aon);
    cpl_imagelist_empty(aoff);
    cpl_imagelist_empty(bon);
    cpl_imagelist_empty(boff);

    return cpl_error_get_code();
}

#define VISIR_SWAP(a, b)\
    do {\
        cpl_imagelist * tmp = a; \
        a = b; \
        b = tmp; \
    } while (0)

/*----------------------------------------------------------------------------*/
/**
  @internal
  @brief    repack nod A and B frame
  @param    fstatea     nodpos A frame state
  @param    fstateb     nodpos B frame state
  @param    products    products frameset
  @param    pbpm        pointer to static bpm, extracted from cube data once
  @param    lintable    linearity correction table
  @return   CPL_ERROR_NONE iff OK, otherwise the relevant CPL error code.
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code
visir_util_repack_two(const repack_framestate * fstatea,
                      const repack_framestate * fstateb,
                      cpl_frameset * products,
                      cpl_image ** pbpm,
                      cpl_image * nonlinear,
                      const cpl_bivector * lintable)
{
    /* process the data */
    const repack_framestate * state = fstatea;
    int chunksize = 200;
    cpl_imagelist * aon = NULL;
    cpl_imagelist * aoff = NULL;
    cpl_imagelist * bon = NULL;
    cpl_imagelist * boff = NULL;
    /* preload lists */
    cpl_imagelist * aon_next = NULL;
    cpl_imagelist * aoff_next = NULL;
    cpl_imagelist * bon_next = NULL;
    cpl_imagelist * boff_next = NULL;

    double qc_bkg_sum = 0.;
    cpl_size qc_bkg_count = 0;

    /* chunksize must be a multiple of the period to avoid having a
     * #on-#off difference larger than chunksize */

    if (visir_data_is_burst(state->datatype)) {
        chunksize = 2 * state->halfcycle *
                    CX_MAX(1, chunksize / (2 * state->halfcycle));
        /* parameter sanity check, ensures equal length A and B lists */
        error_if(state->planeend - state->planestart < 2 * state->halfcycle,
                 CPL_ERROR_ILLEGAL_INPUT,
                 "Number of planes to be repacked must be larger than "
                 "a full cycle of %d planes.", state->halfcycle * 2);
    }


    aon  = cpl_imagelist_new();
    aoff = cpl_imagelist_new();
    bon  = cpl_imagelist_new();
    boff = cpl_imagelist_new();


    /* load the first chunk of data to fill QC parameters*/
    if (state->datatype != VISIR_DATA_AQU_INT) {
        const int pstart = state->planestart;
        const int pend = CX_MIN(pstart + chunksize, state->planeend);
        OMP3_PRAGMA(omp task) {
            load_chunk(aon, aoff, fstatea, pstart, pend);
        }
        OMP3_PRAGMA(omp task) {
            load_chunk(bon, boff, fstateb, pstart, pend);
        }
        OMP3_PRAGMA(omp taskwait);
    }
    if ((visir_data_is_aqu(state->datatype) ||
         visir_data_is_burst(state->datatype)) &&
        state->datatype != VISIR_DATA_AQU_INT) {
        /* cube2 is computed in prepare_output */
        cpl_size llx = 1, lly = 1, urx = 1000000, ury = 1000000;
        /* limit wavelength (PIPE-6744) aqu LR wavelength decreasing in x */
        if (state->resol == VISIR_SPC_R_LRP) {
            llx = 1024 - VISIR_AQU_APPROX_WLEN13;
            urx = 1024 - VISIR_AQU_APPROX_WLEN8;
        }
        qc_bkg_sum += compute_qc_median(aon,  &qc_bkg_count,
                                        llx, lly, urx, ury);
        qc_bkg_sum += compute_qc_median(aoff, &qc_bkg_count,
                                        llx, lly, urx, ury);
        qc_bkg_sum += compute_qc_median(bon,  &qc_bkg_count,
                                        llx, lly, urx, ury);
        qc_bkg_sum += compute_qc_median(boff, &qc_bkg_count,
                                        llx, lly, urx, ury);
    }


    if (state->bkgcorrect == VISIR_SUB_CHOPCORRECT ||
        state->bkgcorrect == VISIR_SUB_NOCORRECT) {
        prepare_output(fstatea, products,
                       qc_bkg_sum / qc_bkg_count, ANOD);
        prepare_output(fstateb, products,
                       qc_bkg_sum / qc_bkg_count, BNOD);
    }
    else if (state->bkgcorrect == VISIR_SUB_CHOPNODCORRECT) {
        prepare_output(state, products,
                       qc_bkg_sum / qc_bkg_count, ABNOD);
    }
    skip_if(0);

    if (state->datatype == VISIR_DATA_AQU_INT) {
        cpl_imagelist * a = cpl_imagelist_new();
        cpl_imagelist * b = cpl_imagelist_new();
        cpl_imagelist * jca = NULL;
        cpl_imagelist * jcb = NULL;
        const cpl_frame * frame;
        const char * filename;
        cpl_image * img;

        frame = irplib_framelist_get_const(fstatea->rawframes,
                                           fstatea->iframe);
        filename = cpl_frame_get_filename(frame);
        img = cpl_image_load(filename, CPL_TYPE_FLOAT, 0, 1);
        cpl_imagelist_set(a, img, cpl_imagelist_get_size(a));

        frame = irplib_framelist_get_const(fstateb->rawframes,
                                           fstateb->iframe);
        filename = cpl_frame_get_filename(frame);
        img = cpl_image_load(filename, CPL_TYPE_FLOAT, 0, 1);
        cpl_imagelist_set(b, img, cpl_imagelist_get_size(b));

        if (fstatea->normalize) {
            cpl_imagelist_multiply_scalar(a, 1. / (state->dit * 4));
            cpl_imagelist_multiply_scalar(b, 1. / (state->dit * 4));
        }

        if (visir_round_to_int(fstatea->offsetx) != 0 ||
            visir_round_to_int(fstatea->offsety) != 0 ||
            visir_round_to_int(fstateb->offsetx) != 0 ||
            visir_round_to_int(fstateb->offsety) != 0) {
            jca = correct_jitter(a, fstatea->offsetx, fstatea->offsety);
            jcb = correct_jitter(b, fstateb->offsetx, fstateb->offsety);
            cpl_imagelist_subtract(jca, jcb);
            cpl_imagelist_subtract(a, b);
            save_subtract_images(fstatea, a, jca, NULL);
        }
        else {
            cpl_imagelist_subtract(a, b);
            save_subtract_images(fstatea, a, jca, NULL);
        }

        cpl_imagelist_delete(a);
        cpl_imagelist_delete(b);
        cpl_imagelist_delete(jca);
        cpl_imagelist_delete(jcb);
    }

    aon_next  = cpl_imagelist_new();
    aoff_next = cpl_imagelist_new();
    bon_next  = cpl_imagelist_new();
    boff_next = cpl_imagelist_new();

    size_t nbytes = 0;
    double t1 = 0;
    double t_loading = 0;
    double t_block_in = 0;

    for (int pstart = state->planestart;
         pstart < state->planeend + chunksize - 1 &&
             state->datatype != VISIR_DATA_AQU_INT;
         pstart += chunksize) {
        const int pend = CX_MIN(pstart + chunksize, state->planeend);

        if (pstart >= pend)
            break;


        /* the process->save part has a throughput of ~200-400MB/s
         * SSDs can match this so by performing one preload in parallel with
         * processing we archive ideal performance also on these systems
         * first chunk already loaded synchronously */
        if (pstart != state->planestart) {
            /* empty the currently worked on lists
             * may have already been emptied by save_ functions */
            cpl_imagelist_empty(aon);
            cpl_imagelist_empty(aoff);
            cpl_imagelist_empty(bon);
            cpl_imagelist_empty(boff);

            /* wait for preload of last iteration to finish */
            double t2 = cpl_test_get_walltime();
            OMP3_PRAGMA(omp taskwait);
            t_block_in += cpl_test_get_walltime() - t2;
            /* swap empty current and full preload lists */
            VISIR_SWAP(aon, aon_next);
            VISIR_SWAP(aoff, aoff_next);
            VISIR_SWAP(bon, bon_next);
            VISIR_SWAP(boff, boff_next);
            t_loading += cpl_test_get_walltime() - t1;
            if (cpl_imagelist_get_size(aon) > 0) {
                cpl_image * img = cpl_imagelist_get(aon, 0);
                nbytes += 4 * cpl_imagelist_get_size(aon) *
                    cpl_image_get_size_x(img) * cpl_image_get_size_y(img) *
                    cpl_type_get_sizeof(cpl_image_get_type(img));
            }
        }

        /* preload the next chunk asynchronous */
        if (pend < state->planeend) {
            int pend_next = CX_MIN(pend + chunksize, state->planeend);
            t1 = cpl_test_get_walltime();
            OMP3_PRAGMA(omp task) {
                load_chunk(aon_next, aoff_next, fstatea, pend, pend_next);
            }
            OMP3_PRAGMA(omp task) {
                load_chunk(bon_next, boff_next, fstateb, pend, pend_next);
            }
        }

        if (*pbpm == NULL && state->datatype == VISIR_DATA_CUBE2) {

            *pbpm = cpl_image_duplicate(cpl_imagelist_get(aon, 0));
            cpl_image_threshold(*pbpm, state->bpmthresh,
                                       state->bpmthresh, 0.0, 1.0);
            skip_if(0);
        }

        correct_linearity(fstatea, aon, lintable, nonlinear);
        correct_linearity(fstatea, aoff, lintable, nonlinear);
        correct_linearity(fstateb, bon, lintable, nonlinear);
        correct_linearity(fstateb, boff, lintable, nonlinear);

        if (state->bkgcorrect == VISIR_SUB_CHOPCORRECT ||
            state->bkgcorrect == VISIR_SUB_CHOPNODCORRECT) {
            cpl_boolean last_chunk = pstart + chunksize >= pend;
            skip_if(save_corrected(fstatea, fstateb,
                                   aon, aoff, bon, boff, last_chunk));
        }
        else {
            skip_if(save_uncorrected(fstatea, fstateb, aon, aoff, bon, boff));
        }
    }
    if (t_loading > 0) {
        cpl_msg_info(cpl_func, "Loading data with, %.3g MB/s.",
                     (nbytes / 1024. / 1024.) / t_loading);
        cpl_msg_info(cpl_func, "Time spent waiting on input: %3.3gs",
                     t_block_in);
    }

    end_skip;
    cpl_imagelist_delete(aon);
    cpl_imagelist_delete(aoff);
    cpl_imagelist_delete(bon);
    cpl_imagelist_delete(boff);
    cpl_imagelist_delete(aon_next);
    cpl_imagelist_delete(aoff_next);
    cpl_imagelist_delete(bon_next);
    cpl_imagelist_delete(boff_next);

    return cpl_error_get_code();
}


/*----------------------------------------------------------------------------*/
/**
  @internal unused
  @brief    Check the consistency of the repacked data
  @param    self The average image
  @param    on   The on-imagelist
  @param    off  The off-imagelist
  @return   CPL_ERROR_NONE iff OK, otherwise the relevant CPL error code.
 */
/*----------------------------------------------------------------------------*/
static cpl_error_code VISIR_ATTR_UNUSED
visir_util_repack_check(const cpl_image     * self,
                        const cpl_imagelist * on,
                        const cpl_imagelist * off)
{
    /* UNUSED due to chunking */
    cpl_image          * meanon  = cpl_imagelist_collapse_create(on);
    cpl_image          * meanoff = cpl_imagelist_collapse_create(off);
    const cpl_error_code err1  = cpl_image_subtract(meanon, meanoff);
    const cpl_error_code err2  = cpl_image_subtract(meanon, self);
    const unsigned bitmask = CPL_STATS_MIN | CPL_STATS_MAX | CPL_STATS_MEAN
        | CPL_STATS_MEDIAN | CPL_STATS_MEDIAN_DEV | CPL_STATS_STDEV;

    cpl_stats * stats = cpl_stats_new_from_image(meanon, bitmask);

    bug_if(err1 + err2);

    bug_if(cpl_stats_dump(stats, bitmask, stderr));

    end_skip;

    cpl_image_delete(meanon);
    cpl_image_delete(meanoff);
    cpl_stats_delete(stats);

    return cpl_error_get_code();

}
