/*
 * This file is part of the KMOS Pipeline
 * Copyright (C) 2002,2003 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

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

#include <string.h>

#include <cpl.h>

#include <kmo_debug.h>
#include <kmo_utils.h>
#include <kmo_dfs.h>
#include <kmo_error.h>
#include <kmo_priv_functions.h>
#include <kmo_cpl_extensions.h>
#include <kmo_constants.h>
#include <kmo_priv_shift.h>

static int kmo_shift_create(cpl_plugin *);
static int kmo_shift_exec(cpl_plugin *);
static int kmo_shift_destroy(cpl_plugin *);
static int kmo_shift(cpl_parameterlist *, cpl_frameset *);

static char kmo_shift_description[] =
"This recipe shifts a cube spatially. A positive x-shift shifts the data to the\n"
"left, a positive y-shift shifts upwards, where a shift of one pixel equals\n"
"0.2arcsec. The output will still have the same dimensions, but the borders \n"
"will be filled with NaNs accordingly.\n"
"To adjust only the WCS without moving the data the --wcs-only parameter has to\n"
"be set to TRUE. The WCS is updated in the same way as if the data would have\n"
"moved as well. This means that the point at (x,y) has the same coordinates as\n"
"the point (x+1,y+1) after updating the WCS (the WCS moved in the opposite\n"
"direction).\n"
"\n"
"BASIC PARAMETERS:\n"
"-----------------\n"
"--shifts\n"
"This parameter must be supplied. It contains the amount of shift to apply. The\n"
"unit is in arcsec. If the --shifts parameter contains only two values (x,y),\n"
"all IFUs will be shifted by the same amount. If it contains 48 values\n"
"(x1,y1;x2,y2;...;x24,y24), the IFUs are shifted individually.\n"
"\n"
"--imethod\n"
"The interpolation method to apply when the shift value isn’t a multiple of the\n"
"pixel scale. There are two methods available:\n"
"   * BCS: Bicubic spline\n"
"   * NN:  Nearest Neighbor\n"
"\n"
"--ifu\n"
"If a single IFU should be shifted, it can be defined using the --ifu parameter\n"
"(--shifts parameter contains only two values).\n"
"\n"
"ADVANCED PARAMETERS\n"
"-------------------\n"
"--flux\n"
"Specify if flux conservation should be applied.\n"
"\n"
"--extrapolate\n"
"By default no extrapolation is applied. This means that the output frame will\n"
"shrink at most one pixel, because the data is shifted out of the frame. When\n"
"turning extrapolation on, the size of the output frame stays the same as for\n"
"the input frame.\n"
"\n"
"--wcs-only\n"
"By default data and WCS are shifted in sync. If this parameter is set to TRUE\n"
"only the WCS is updated (i.e. if someone thinks that the IFU isn't pointing\n"
"exactly to the correct coordinates.)\n"
"\n"
"-------------------------------------------------------------------------------\n"
"  Input files:\n"
"\n"
"   DO                    KMOS                                                  \n"
"   category              Type   Explanation                    Required #Frames\n"
"   --------              -----  -----------                    -------- -------\n"
"   <none or any>         F3I    data frame                        Y        1   \n"
"\n"
"  Output files:\n"
"\n"
"   DO                    KMOS\n"
"   category              Type   Explanation\n"
"   --------              -----  -----------\n"
"   SHIFT                 F3I    Shifted data cube\n"
"-------------------------------------------------------------------------------\n"
"\n";

/**
 * @defgroup kmo_shift kmo_shift Shift a cube spatially
 *
 * See recipe description for details.
 */

/**@{*/

/**
  @brief    Build the list of available plugins, for this module. 
  @param    list    the plugin list
  @return   0 if everything is ok, -1 otherwise

  Create the recipe instance and make it available to the application using the 
  interface. This function is exported.
*/
int cpl_plugin_get_info(cpl_pluginlist *list)
{
    cpl_recipe *recipe = cpl_calloc(1, sizeof *recipe);
    cpl_plugin *plugin = &recipe->interface;

    cpl_plugin_init(plugin,
                        CPL_PLUGIN_API,
                        KMOS_BINARY_VERSION,
                        CPL_PLUGIN_TYPE_RECIPE,
                        "kmo_shift",
                        "Shift a cube spatially",
                        kmo_shift_description,
                        "Alex Agudo Berbel",
                        "usd-help@eso.org",
                        kmos_get_license(),
                        kmo_shift_create,
                        kmo_shift_exec,
                        kmo_shift_destroy);

    cpl_pluginlist_append(list, plugin);

    return 0;
}

/**
  @brief    Setup the recipe options    
  @param    plugin  the plugin
  @return   0 if everything is ok

  Defining the command-line/configuration parameters for the recipe.
*/
static int kmo_shift_create(cpl_plugin *plugin)
{
    cpl_recipe *recipe;
    cpl_parameter *p;

    /* Check that the plugin is part of a valid recipe */
    if (cpl_plugin_get_type(plugin) == CPL_PLUGIN_TYPE_RECIPE) 
        recipe = (cpl_recipe *)plugin;
    else
        return -1;

    /* Create the parameters list in the cpl_recipe object */
    recipe->parameters = cpl_parameterlist_new();

    /* Fill the parameters list */
    /* --imethod */
    p = cpl_parameter_new_value("kmos.kmo_shift.imethod",
                                CPL_TYPE_STRING,
                                "Method to use for interpolation.\n"
                                "[\"BCS\" (bicubic spline, default), "
                                "\"NN\" (nearest neighbor)]",
                                "kmos.kmo_shift",
                                "BCS");
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "imethod");
    cpl_parameter_disable(p, CPL_PARAMETER_MODE_ENV);
    cpl_parameterlist_append(recipe->parameters, p);

    /* --extrapolate */
    p = cpl_parameter_new_value("kmos.kmo_shift.extrapolate",
                                CPL_TYPE_BOOL,
                                "Applies only to 'method=BCS' when doing sub-"
                                "pixel shifts: "
                                "FALSE: shifted IFU will be filled with NaN's "
                                "at the borders,"
                                "TRUE: shifted IFU will be extrapolated at "
                                "the borders",
                                "kmos.kmo_shift",
                                FALSE);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "extrapolate");
    cpl_parameter_disable(p, CPL_PARAMETER_MODE_ENV);
    cpl_parameterlist_append(recipe->parameters, p);

    /* --shifts */
    p = cpl_parameter_new_value("kmos.kmo_shift.shifts",
                                CPL_TYPE_STRING,
                                "The shifts for each spatial dimension for all "
                                "specified IFUs."
                                "\"x1,y1;x2,y2;...\" (arcsec)",
                                "kmos.kmo_shift",
                                "");
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "shifts");
    cpl_parameter_disable(p, CPL_PARAMETER_MODE_ENV);
    cpl_parameterlist_append(recipe->parameters, p);

    /* --ifu */
    p = cpl_parameter_new_value("kmos.kmo_shift.ifu",
                                CPL_TYPE_INT,
                                "The IFU to shift [1 to 24] or shift all IFUs "
                                "Default value of 0 applies shift to all IFUs.",
                                "kmos.kmo_shift",
                                0);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "ifu");
    cpl_parameter_disable(p, CPL_PARAMETER_MODE_ENV);
    cpl_parameterlist_append(recipe->parameters, p);

    /* --flux */
    p = cpl_parameter_new_value("kmos.kmo_shift.flux",
                                CPL_TYPE_BOOL,
                                "Apply flux conservation: "
                                "(TRUE (apply) or "
                                "FALSE (don't apply)",
                                "kmos.kmo_shift",
                                FALSE);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "flux");
    cpl_parameter_disable(p, CPL_PARAMETER_MODE_ENV);
    cpl_parameterlist_append(recipe->parameters, p);

    /* --wcs-only */
    p = cpl_parameter_new_value("kmos.kmo_shift.wcs-only",
                                CPL_TYPE_BOOL,
                                "FALSE: if data and wcs should be updated."
                                "TRUE: if only wcs should be corrected",
                                "kmos.kmo_shift",
                                0);
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "wcs-only");
    cpl_parameter_disable(p, CPL_PARAMETER_MODE_ENV);
    cpl_parameterlist_append(recipe->parameters, p);

    return 0;
}

/**
  @brief    Execute the plugin instance given by the interface
  @param    plugin  the plugin
  @return   0 if everything is ok
*/
static int kmo_shift_exec(cpl_plugin *plugin)
{
    cpl_recipe  *recipe;

    /* Get the recipe out of the plugin */
    if (cpl_plugin_get_type(plugin) == CPL_PLUGIN_TYPE_RECIPE) 
        recipe = (cpl_recipe *)plugin;
    else return -1 ;

    return kmo_shift(recipe->parameters, recipe->frames);
}

/**
  @brief    Destroy what has been created by the 'create' function
  @param    plugin  the plugin
  @return   0 if everything is ok
*/
static int kmo_shift_destroy(cpl_plugin *plugin)
{
    cpl_recipe *recipe;

    /* Get the recipe out of the plugin */
    if (cpl_plugin_get_type(plugin) == CPL_PLUGIN_TYPE_RECIPE) 
        recipe = (cpl_recipe *)plugin;
    else return -1 ;

    cpl_parameterlist_delete(recipe->parameters);
    return 0 ;
}

/**
  @brief    Interpret the command line options and execute the data processing
  @param    parlist     the parameters list
  @param    frameset   the frames list
  @return   0 if everything is ok

  Possible _cpl_error_code_ set in this function:

    @li CPL_ERROR_ILLEGAL_INPUT      if operator not valid,
                                     if first operand not 3d or
                                     if second operand not valid
    @li CPL_ERROR_INCOMPATIBLE_INPUT if the dimensions of the two operands do
                                     not match
*/
static int kmo_shift(cpl_parameterlist *parlist, cpl_frameset *frameset)
{
    const char       *method             = NULL,
                     *shifts_txt         = NULL;

    cpl_imagelist    *data               = NULL,
                     *noise              = NULL;

    cpl_vector       *shifts             = NULL,
                     *shifts2            = NULL;

    int              ret_val             = 0,
                     nr_devices          = 0,
                     i                   = 0,
                     valid_ifu           = FALSE,
                     flux                = FALSE,
                     size                = 0,
                     ifu                 = 0,
                     extrapolate         = 0,
                     wcs_only            = FALSE,
                     devnr               = 0,
                     index_data          = 0,
                     index_noise         = 0;

    enum extrapolationType extrapol_enum = 0;

    const double     *pshifts2           = NULL;

    cpl_propertylist *sub_header_data    = NULL,
                     *sub_header_noise   = NULL;

    cpl_frame        *frame              = NULL;

    main_fits_desc   desc1;

    KMO_TRY
    {
        kmo_init_fits_desc(&desc1);

        /* --- check input --- */
        KMO_TRY_ASSURE((parlist != NULL) &&
                       (frameset != NULL),
                       CPL_ERROR_NULL_INPUT,
                       "Not all input data is provided!");

        KMO_TRY_ASSURE(cpl_frameset_get_size(frameset) == 1,
                       CPL_ERROR_NULL_INPUT,
                       "A cube must be provided!");

        KMO_TRY_ASSURE(kmo_dfs_set_groups(frameset) == 1,
                       CPL_ERROR_ILLEGAL_INPUT,
                       "Cannot identify RAW and CALIB frames!");

        cpl_msg_info("", "--- Parameter setup for kmo_shift ---------");

        KMO_TRY_EXIT_IF_NULL(
            method = kmo_dfs_get_parameter_string(parlist,
                                           "kmos.kmo_shift.imethod"));
        KMO_TRY_EXIT_IF_ERROR(
            kmo_dfs_print_parameter_help(parlist, "kmos.kmo_shift.imethod"));

        extrapolate = kmo_dfs_get_parameter_bool(parlist,
                                                "kmos.kmo_shift.extrapolate");
        KMO_TRY_CHECK_ERROR_STATE();

        if (strcmp(method, "NN") == 0) {
            extrapol_enum = NONE_NANS;
        } else if (strcmp(method, "BCS") == 0) {
            if (extrapolate == FALSE) {
                extrapol_enum = NONE_NANS;
            } else if (extrapolate == TRUE) {
                extrapol_enum = BCS_NATURAL;
            } else {
                KMO_TRY_ASSURE(1 == 0,
                               CPL_ERROR_ILLEGAL_INPUT,
                               "extrapolate must either FALSE or TRUE!");
            }
        } else {
            KMO_TRY_ASSURE(1 == 0,
                           CPL_ERROR_ILLEGAL_INPUT,
                           "method must be either \"BCS\" or \"NN\" !");
        }

        KMO_TRY_EXIT_IF_ERROR(
            kmo_dfs_print_parameter_help(parlist, "kmos.kmo_shift.extrapolate"));

        shifts_txt = kmo_dfs_get_parameter_string(parlist,
                                                  "kmos.kmo_shift.shifts");
        KMO_TRY_CHECK_ERROR_STATE();
        KMO_TRY_EXIT_IF_ERROR(
            kmo_dfs_print_parameter_help(parlist, "kmos.kmo_shift.shifts"));

        KMO_TRY_ASSURE(strcmp(shifts_txt, "") != 0,
                       CPL_ERROR_ILLEGAL_INPUT,
                       "At least two values for --shifts parameter must be "
                       "provided!");

        shifts = kmo_identify_ranges(shifts_txt);
        KMO_TRY_CHECK_ERROR_STATE();

        size = cpl_vector_get_size(shifts);
        KMO_TRY_CHECK_ERROR_STATE();

        KMO_TRY_ASSURE((size % 2) == 0,
                       CPL_ERROR_ILLEGAL_INPUT,
                       "shifts parameter must have an even number of elements!");

        ifu = kmo_dfs_get_parameter_int(parlist, "kmos.kmo_shift.ifu");
        KMO_TRY_CHECK_ERROR_STATE();
        KMO_TRY_EXIT_IF_ERROR(
            kmo_dfs_print_parameter_help(parlist, "kmos.kmo_shift.ifu"));

        if (ifu == 0) {
            // shift all IFUs the same or different amounts
            KMO_TRY_ASSURE((size == 2) || (size == 2*KMOS_NR_IFUS),
                           CPL_ERROR_ILLEGAL_INPUT,
                           "shifts parameter must have exactly 2 elements"
                           "(shift all IFUs the same amount) or 48 elements "
                           "(shift all IFUs individually)!");
        } else {
            // shift only one specific IFU
            KMO_TRY_ASSURE(size == 2,
                           CPL_ERROR_ILLEGAL_INPUT,
                           "shifts parameter must have exactly 2 elements to "
                           "shift a single IFU!");
        }

        // setup a vector of length 48 regardless of how many IFUs to shift
        if (size == 2*KMOS_NR_IFUS) {
            KMO_TRY_EXIT_IF_NULL(
                shifts2 = cpl_vector_duplicate(shifts));
        } else {
            KMO_TRY_EXIT_IF_NULL(
                shifts2 = cpl_vector_new(2*KMOS_NR_IFUS));
            KMO_TRY_EXIT_IF_NULL(
                pshifts2 = cpl_vector_get_data_const(shifts));
            for (i = 0; i < KMOS_NR_IFUS; i++) {
                cpl_vector_set(shifts2, 2*i, pshifts2[0]);
                cpl_vector_set(shifts2, 2*i+1, pshifts2[1]);
            }
        }

        KMO_TRY_EXIT_IF_NULL(
                pshifts2 = cpl_vector_get_data_const(shifts2));

        flux = kmo_dfs_get_parameter_bool(parlist,
                                          "kmos.kmo_shift.flux");
        KMO_TRY_CHECK_ERROR_STATE();
        KMO_TRY_EXIT_IF_ERROR(
            kmo_dfs_print_parameter_help(parlist, "kmos.kmo_shift.flux"));

        KMO_TRY_ASSURE((flux == TRUE) || (flux == FALSE),
                       CPL_ERROR_ILLEGAL_INPUT,
                       "flux must be TRUE or FALSE!");

        wcs_only = kmo_dfs_get_parameter_bool(parlist, "kmos.kmo_shift.wcs-only");
        KMO_TRY_CHECK_ERROR_STATE();
        KMO_TRY_EXIT_IF_ERROR(
            kmo_dfs_print_parameter_help(parlist, "kmos.kmo_shift.wcs-only"));

        KMO_TRY_ASSURE((wcs_only == TRUE) || (wcs_only == FALSE),
                       CPL_ERROR_NULL_INPUT,
                       "wcs_only must be TRUE or FALSE!");

        cpl_msg_info("", "-------------------------------------------");

        KMO_TRY_ASSURE((flux == 0) ||
                       (flux == 1),
                       CPL_ERROR_ILLEGAL_INPUT,
                       "flux must be either 0 or 1 !");

        // load descriptor of first operand
        KMO_TRY_EXIT_IF_NULL(
            frame = kmo_dfs_get_frame(frameset, "0"));

        desc1 = kmo_identify_fits_header(
                    cpl_frame_get_filename(frame));
        KMO_TRY_CHECK_ERROR_STATE_MSG("Provided fits file doesn't seem to be "
                                      "in KMOS-format!");

        KMO_TRY_ASSURE(desc1.fits_type == f3i_fits,
                       CPL_ERROR_ILLEGAL_INPUT,
                       "First input file hasn't correct data type "
                       "(KMOSTYPE must be F3I)!");

        // --- load, update & save primary header ---
        KMO_TRY_EXIT_IF_ERROR(
            kmo_dfs_save_main_header(frameset, SHIFT, "", frame, NULL,
                                     parlist, cpl_func));

        // --- load data ---
        if (desc1.ex_noise == TRUE) {
            nr_devices = desc1.nr_ext / 2;
        } else {
            nr_devices = desc1.nr_ext;
        }

        for (i = 1; i <= nr_devices; i++) {
            if (desc1.ex_noise == FALSE) {
                devnr = desc1.sub_desc[i - 1].device_nr;
            } else {
                devnr = desc1.sub_desc[2 * i - 1].device_nr;
            }

            if (desc1.ex_badpix == FALSE) {
                index_data = kmo_identify_index_desc(desc1, devnr, FALSE);
            } else {
                index_data = kmo_identify_index_desc(desc1, devnr, 2);
            }
            KMO_TRY_CHECK_ERROR_STATE();

            if (desc1.ex_noise) {
                index_noise = kmo_identify_index_desc(desc1, devnr, TRUE);
            }
            KMO_TRY_CHECK_ERROR_STATE();

            KMO_TRY_EXIT_IF_NULL(
                sub_header_data = kmo_dfs_load_sub_header(frameset, "0", devnr,
                                                          FALSE));

            // check if IFU is valid
            valid_ifu = FALSE;
            if (desc1.sub_desc[index_data-1].valid_data == TRUE) {
                valid_ifu = TRUE;
            }

            if (desc1.ex_noise) {
                // load noise anyway since we have to save it in the output
                KMO_TRY_EXIT_IF_NULL(
                    sub_header_noise = kmo_dfs_load_sub_header(frameset, "0",
                                                               devnr, TRUE));
            }

            if (valid_ifu) {
                // load data
                KMO_TRY_EXIT_IF_NULL(
                    data = kmo_dfs_load_cube(frameset, "0", devnr, FALSE));

                // load noise, if existing
                if (desc1.ex_noise && desc1.sub_desc[index_noise-1].valid_data) {
                    KMO_TRY_EXIT_IF_NULL(
                        noise = kmo_dfs_load_cube(frameset, "0", devnr, TRUE));
                }

                if ((ifu == 0) || (ifu == devnr)) {
                    // process here
                    KMO_TRY_EXIT_IF_ERROR(
                        kmo_priv_shift(&data, &noise,
                                       &sub_header_data, &sub_header_noise,
                                       pshifts2[2*i-2] / KMOS_PIX_RESOLUTION,
                                       pshifts2[2*i-1] / KMOS_PIX_RESOLUTION,
                                       flux, devnr, method, extrapol_enum,
                                       wcs_only));
                } else {
                    // leave data and noise as they are and
                    // save them again unshifted
                }

                // save data and noise (if existing)
                KMO_TRY_EXIT_IF_ERROR(
                    kmo_dfs_save_cube(data, SHIFT, "", sub_header_data, 0./0.));

                if (desc1.ex_noise) {
                    KMO_TRY_EXIT_IF_ERROR(
                        kmo_dfs_save_cube(noise, SHIFT, "", sub_header_noise, 0./0.));
                }

                // free memory
                cpl_imagelist_delete(data); data = NULL;
                cpl_imagelist_delete(noise); noise = NULL;
            } else {
                // invalid IFU, just save sub_headers
                KMO_TRY_EXIT_IF_ERROR(
                    kmo_dfs_save_sub_header(SHIFT, "", sub_header_data));

                if (desc1.ex_noise == TRUE) {
                    KMO_TRY_EXIT_IF_ERROR(
                        kmo_dfs_save_sub_header(SHIFT, "", sub_header_noise));
                }
            }

            // free memory
            cpl_propertylist_delete(sub_header_data); sub_header_data = NULL;
            cpl_propertylist_delete(sub_header_noise); sub_header_noise = NULL;
        }
    }
    KMO_CATCH
    {
        KMO_CATCH_MSG();
        ret_val = -1;
    }

    kmo_free_fits_desc(&desc1);
    cpl_propertylist_delete(sub_header_data); sub_header_data = NULL;
    cpl_propertylist_delete(sub_header_noise); sub_header_noise = NULL;
    cpl_imagelist_delete(data); data = NULL;
    cpl_imagelist_delete(noise); noise = NULL;
    cpl_vector_delete(shifts); shifts = NULL;
    cpl_vector_delete(shifts2); shifts2 = NULL;

    return ret_val;
}

/**@}*/
