/*
 * 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_rotate.h>

static int kmo_rotate_create(cpl_plugin *);
static int kmo_rotate_exec(cpl_plugin *);
static int kmo_rotate_destroy(cpl_plugin *);
static int kmo_rotate(cpl_parameterlist *, cpl_frameset *);

static char kmo_rotate_description[] =
"This recipe rotates a cube spatially (CCW). If the rotation angle isn't\n"
"a multiple of 90 degrees, the output cube will be interpolated and get larger\n"
"accordingly.\n"
"By default all IFUs will be rotated.\n"
"\n"
"BASIC PARAMETERS:\n"
"-----------------\n"
"--rotations\n"
"This parameter must be supplied. It contains the amount of rotation to apply.\n"
"The unit is in degrees. If it contains one value (e.g. “3.5”) all IFUs are\n"
"rotated by the same amount. If 24 values are supplied each IFU is rotated\n"
"individually (e.g. “2.3;15.7;…;-3.3”).\n"
"\n"
"--imethod\n"
"The interpolation method to apply when rotating an angle not being a multiple\n"
"of 90. There are two methods available:\n"
"   * BCS: Bicubic spline\n"
"   * NN:  Nearest Neighbor (currently disabled)\n"
"\n"
"--ifu\n"
"If a single IFU should be rotated, it can be defined using the --ifu parameter\n"
"(--rotations parameter contains only one value).\n"
"\n"
"ADVANCED PARAMETERS\n"
"-------------------\n"
"--flux\n"
"Specify if flux conservation should be applied.\n"
"\n"
"--extrapolate\n"
"By default the output frame grows when rotating an angle not being a multiple\n"
"of 90. In this case none of the input data is lost. When it is desired to keep\n"
"the same size as the input frame this parameter can be set to TRUE and the\n"
"data will be clipped.\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"
"   ROTATE                F3I    Rotated data cube\n"
"-------------------------------------------------------------------------------\n"
"\n";

/**
 * @defgroup kmo_rotate kmo_rotate Rotate 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_rotate",
                        "Rotate a cube spatially",
                        kmo_rotate_description,
                        "Alex Agudo Berbel",
                        "usd-help@eso.org",
                        kmos_get_license(),
                        kmo_rotate_create,
                        kmo_rotate_exec,
                        kmo_rotate_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_rotate_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_rotate.imethod",
                                CPL_TYPE_STRING,
                                "Method to use for interpolation: "
                                "[\"BCS\" (bicubic spline, default), "
                                "\"NN\" (nearest neighbor), not implemented yet]",
                                "kmos.kmo_rotate",
                                "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_rotate.extrapolate",
                                CPL_TYPE_BOOL,
                                "Applies only when rotation angle is different "
                                "from multiples of 90 degrees: "
                                "FALSE: Output IFU will be larger than the input "
                                "(Default), "
                                "TRUE: The size of input and output IFU remains "
                                "the same. Data will be clipped.",
                                "kmos.kmo_rotate",
                                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);

    /* --rotations */
    p = cpl_parameter_new_value("kmos.kmo_rotate.rotations",
                                CPL_TYPE_STRING,
                                "The rotations for all specified IFUs. "
                                "\"rot1;rot2;...\" (degrees)",
                                "kmos.kmo_rotate",
                                "");
    cpl_parameter_set_alias(p, CPL_PARAMETER_MODE_CLI, "rotations");
    cpl_parameter_disable(p, CPL_PARAMETER_MODE_ENV);
    cpl_parameterlist_append(recipe->parameters, p);

    /* --ifu */
    p = cpl_parameter_new_value("kmos.kmo_rotate.ifu",
                                CPL_TYPE_INT,
                                "The IFU to rotate [1 to 24] or rotate all IFUs "
                                "[0, default].",
                                "kmos.kmo_rotate",
                                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_rotate.flux",
                                CPL_TYPE_BOOL,
                                "Apply flux conservation: "
                                "(TRUE (apply) or "
                                "FALSE (don't apply)",
                                "kmos.kmo_rotate",
                                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);

    return 0;
}

/**
  @brief    Execute the plugin instance given by the interface
  @param    plugin  the plugin
  @return   0 if everything is ok
*/
static int kmo_rotate_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_rotate(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_rotate_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_rotate(cpl_parameterlist *parlist, cpl_frameset *frameset)
{
    const char       *method             = NULL,
                     *rotations_txt      = NULL;

    cpl_imagelist    *data               = NULL,
                     *noise              = NULL;

    cpl_vector       *rotations          = NULL,
                     *rotations2         = NULL;

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

    enum extrapolationType extrapol_enum = 0;

    const double     *protations2        = 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_rotate --------");

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

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

        if (extrapolate == 1) {
            extrapol_enum = NONE_NANS;
        } else if (extrapolate == 0) {
            extrapol_enum = RESIZE_NANS;
        } else {
            KMO_TRY_ASSURE(1 == 0,
                           CPL_ERROR_ILLEGAL_INPUT,
                           "extrapolate must be 1 or 0!");
        }

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

        rotations_txt = kmo_dfs_get_parameter_string(parlist,
                                                  "kmos.kmo_rotate.rotations");
        KMO_TRY_CHECK_ERROR_STATE();
        KMO_TRY_EXIT_IF_ERROR(
            kmo_dfs_print_parameter_help(parlist, "kmos.kmo_rotate.rotations"));

        KMO_TRY_ASSURE(strcmp(rotations_txt, "") != 0,
                       CPL_ERROR_ILLEGAL_INPUT,
                       "At least one value for --rotations parameter must be "
                       "provided!");

        rotations = kmo_identify_values(rotations_txt);
        KMO_TRY_CHECK_ERROR_STATE();

        size = cpl_vector_get_size(rotations);
        KMO_TRY_CHECK_ERROR_STATE();

        KMO_TRY_ASSURE((size == 1) || (size == KMOS_NR_IFUS),
                       CPL_ERROR_ILLEGAL_INPUT,
                       "rotations parameter must have either one or 24 elements!");

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

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

        // setup a vector of length 24 regardless of how many IFUs to rotate
        if (size == KMOS_NR_IFUS) {
            KMO_TRY_EXIT_IF_NULL(
                rotations2 = cpl_vector_duplicate(rotations));
        } else {
            KMO_TRY_EXIT_IF_NULL(
                rotations2 = cpl_vector_new(KMOS_NR_IFUS));
            KMO_TRY_EXIT_IF_NULL(
                protations2 = cpl_vector_get_data_const(rotations));
            for (i = 0; i < KMOS_NR_IFUS; i++) {
                cpl_vector_set(rotations2, i, protations2[0]);
            }
        }

        KMO_TRY_EXIT_IF_NULL(
                protations2 = cpl_vector_get_data_const(rotations2));

        KMO_TRY_ASSURE((strcmp(method, "NN") == 0) ||
                       (strcmp(method, "BCS") == 0)
                       /*(strcmp(method, "kriging") == 0) ||
                       (strcmp(method, "cubic") == 0) ||
                       (strcmp(method, "shepard") == 0) ||
                       (strcmp(method, "drizzle") == 0)*/,
                       CPL_ERROR_ILLEGAL_INPUT,
                       "method must be \"BCS\"!");

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

        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, ROTATE, "", 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_rotate(&data, &noise,
                                        &sub_header_data, &sub_header_noise,
                                        protations2[i-1],
                                        flux, devnr, method, extrapol_enum));
                } else {
                    // leave data and noise as they are and
                    // save them again unrotated
                }

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

                if (desc1.ex_noise) {
                    KMO_TRY_EXIT_IF_ERROR(
                        kmo_dfs_save_cube(noise, ROTATE, "", 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(ROTATE, "", sub_header_data));

                if (desc1.ex_noise) {
                    KMO_TRY_EXIT_IF_ERROR(
                        kmo_dfs_save_sub_header(ROTATE, "", 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(rotations); rotations = NULL;
    cpl_vector_delete(rotations2); rotations2 = NULL;

    return ret_val;
}

/**@}*/
