/*
 * This file is part of the GIRAFFE Pipeline
 * Copyright (C) 2002-2019 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  02110-1301  USA
 */

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

#include <stdlib.h>
#include <math.h>

#include <cxmap.h>
#include <cxstring.h>
#include <cxstrutils.h>

#include <cpl_error.h>
#include <cpl_propertylist.h>

#include "gialias.h"
#include "gierror.h"
#include "giimage.h"
#include "gitable.h"
#include "gichebyshev.h"
#include "giwlsolution.h"


/**
 * @defgroup giwlsolution Wavelength Solution
 *
 * TBD
 */

/**@{*/

struct GiWlSolution {

    GiModel *model;

    cxbool subslits;
    GiWlResiduals *residuals;

};


inline static GiWlSolution *
_giraffe_wlsolution_new(const cxchar *name)
{

    GiWlSolution *self = cx_calloc(1, sizeof *self);


    if (self) {

        self->model = giraffe_model_new(name);

        if (self->model == NULL) {
            giraffe_wlsolution_delete(self);
            return NULL;
        }

        if (giraffe_model_get_type(self->model) != GI_MODEL_XOPT) {
            giraffe_wlsolution_delete(self);
            return NULL;
        }

        self->subslits = FALSE;
        self->residuals = NULL;

    }

    return self;

}


GiWlSolution *
giraffe_wlsolution_new(const cxchar *name, cxint orientation, cxint npixels,
                       cxdouble pixelsize, GiGrating *grating)
{

    GiWlSolution *self = NULL;


    if (name == NULL) {
        return self;
    }

    if (grating == NULL) {
        return self;
    }


    self = _giraffe_wlsolution_new(name);

    if (self) {

        orientation = orientation < 0 ? -npixels : npixels;
        pixelsize /= 1000.;

        giraffe_error_push();

        giraffe_model_set_parameter(self->model, "Orientation",
                                    orientation);
        giraffe_model_set_parameter(self->model, "Order",
                                    grating->order);
        giraffe_model_set_parameter(self->model, "PixelSize",
                                    pixelsize);
        giraffe_model_set_parameter(self->model, "FocalLength",
                                    grating->fcoll);
        giraffe_model_set_parameter(self->model, "Magnification",
                                    grating->gcam);
        giraffe_model_set_parameter(self->model, "Angle",
                                    grating->theta);
        giraffe_model_set_parameter(self->model, "Spacing",
                                    grating->space);

        if (strcmp(name, "xoptmod2") == 0) {

            giraffe_model_set_parameter(self->model, "Sdx", grating->sdx);
            giraffe_model_set_parameter(self->model, "Sdy", grating->sdy);
            giraffe_model_set_parameter(self->model, "Sphi", grating->sphi);

        }

        if (cpl_error_get_code() != CPL_ERROR_NONE) {
            giraffe_wlsolution_delete(self);
            return NULL;
        }

        giraffe_error_pop();

    }

    return self;

}


/**
 * @brief
 *   Create a new wavelength solution from another wavelength solution.
 *
 * @param other  The wavelength solution to be copied.
 *
 * @return
 *   The function returns the newly created wavelength solution, or @c NULL
 *   if an error occurred.
 *
 * TBD
 */

GiWlSolution *
giraffe_wlsolution_clone(const GiWlSolution *other)
{

    GiWlSolution *self = NULL;


    if (other != NULL) {

        self = cx_calloc(1, sizeof(GiWlSolution));

        self->model = giraffe_model_clone(other->model);

        self->subslits = other->subslits;
        self->residuals = giraffe_wlresiduals_clone(other->residuals);

    }

    return self;

}


/**
 * @brief
 *   Create a new wavelength solution from a wavelength solution table.
 *
 * @param spectra   Reference spectrum.
 * @param solution  Wavelength solution table.
 * @param grating   Grating setup information.
 *
 * @return
 *   The function returns the newly created wavelength solution, or @c NULL
 *   if an error occurred.
 *
 * TBD
 */

GiWlSolution *
giraffe_wlsolution_create(GiTable *solution, GiImage *spectra,
                          GiGrating *grating)
{

    const cxchar *name = NULL;

    cxint npixels = 0;
    cxint orientation = 0;

    cxdouble pixelsize = 0.;
    cxdouble fcoll = 0.;
    cxdouble gcam = 0.;
    cxdouble theta = 0.;
    cxdouble sdx = 0.;
    cxdouble sdy = 0.;
    cxdouble sphi = 0.;


    cpl_propertylist *properties = NULL;

    GiWlSolution *self = NULL;



    if (solution == NULL) {
        return NULL;
    }

    if (giraffe_table_get_properties(solution) == NULL) {
        return NULL;
    }

    if (giraffe_table_get(solution) == NULL) {
        return NULL;
    }


    if (spectra == NULL) {
        return NULL;
    }

    if (giraffe_image_get_properties(spectra) == NULL) {
        return NULL;
    }

    if (giraffe_image_get(spectra) == NULL) {
        return NULL;
    }


    if (grating == NULL) {
        return NULL;
    }


    /*
     * Setup the optical model from the wavelength solution table properties,
     * the grating setup and the reference spectrum.
     */

    /*
     * Reference image: number of pixels and pixel size
     */

    properties = giraffe_image_get_properties(spectra);

    if (!cpl_propertylist_has(properties, GIALIAS_PIXSIZX)) {
        return NULL;
    }
    else {

        /*
         * Get pixel size and convert it from microns to mm.
         */

        pixelsize = cpl_propertylist_get_double(properties, GIALIAS_PIXSIZX);
        pixelsize /= 1000.;

    }

    npixels = cpl_image_get_size_y(giraffe_image_get(spectra));


    /*
     * Wavelength solution properties: orientation, collimator focal
     * length, camera magnification, grating angle and slit offsets.
     */

    properties = giraffe_table_get_properties(solution);


    if (!cpl_propertylist_has(properties, GIALIAS_WSOL_OMNAME)) {
        return NULL;
    }
    else {
        name = cpl_propertylist_get_string(properties, GIALIAS_WSOL_OMNAME);
    }


    self = _giraffe_wlsolution_new(name);

    if (self) {

        if (!cpl_propertylist_has(properties, GIALIAS_WSOL_SUBSLITS)) {
            giraffe_wlsolution_delete(self);
            return NULL;
        }
        else {

            self->subslits = cpl_propertylist_get_bool(properties,
                                                       GIALIAS_WSOL_SUBSLITS);

        }

        if (!cpl_propertylist_has(properties, GIALIAS_WSOL_OMDIR)) {
            giraffe_wlsolution_delete(self);
            return NULL;
        }
        else {
            orientation = cpl_propertylist_get_int(properties,
                                                   GIALIAS_WSOL_OMDIR);
            orientation = orientation < 0 ? -fabs(npixels) : fabs(npixels);
        }


        if (!cpl_propertylist_has(properties, GIALIAS_WSOL_OMFCOLL)) {
            giraffe_wlsolution_delete(self);
            return NULL;
        }
        else {
            fcoll = cpl_propertylist_get_double(properties,
                                                GIALIAS_WSOL_OMFCOLL);
        }


        if (!cpl_propertylist_has(properties, GIALIAS_WSOL_OMGCAM)) {
            giraffe_wlsolution_delete(self);
            return NULL;
        }
        else {
            gcam = cpl_propertylist_get_double(properties,
                                               GIALIAS_WSOL_OMGCAM);
        }


        if (!cpl_propertylist_has(properties, GIALIAS_WSOL_OMGTHETA)) {
            giraffe_wlsolution_delete(self);
            return NULL;
        }
        else {
            theta = cpl_propertylist_get_double(properties,
                                                GIALIAS_WSOL_OMGTHETA);
        }


        if (strcmp(name, "xoptmod2") == 0) {

            if (!cpl_propertylist_has(properties, GIALIAS_WSOL_OMSDX)) {
                giraffe_wlsolution_delete(self);
                return NULL;
            }
            else {
                sdx = cpl_propertylist_get_double(properties,
                                                  GIALIAS_WSOL_OMSDX);
            }


            if (!cpl_propertylist_has(properties, GIALIAS_WSOL_OMSDY)) {
                giraffe_wlsolution_delete(self);
                return NULL;
            }
            else {
                sdy = cpl_propertylist_get_double(properties,
                                                  GIALIAS_WSOL_OMSDY);
            }


            if (!cpl_propertylist_has(properties, GIALIAS_WSOL_OMSPHI)) {
                giraffe_wlsolution_delete(self);
                return NULL;
            }
            else {
                sphi = cpl_propertylist_get_double(properties,
                                                   GIALIAS_WSOL_OMSPHI);
            }

        }


        /*
         * Initialize the optical model parameters
         */

        giraffe_error_push();

        giraffe_model_set_parameter(self->model, "Orientation", orientation);
        giraffe_model_set_parameter(self->model, "Order", grating->order);
        giraffe_model_set_parameter(self->model, "PixelSize", pixelsize);
        giraffe_model_set_parameter(self->model, "FocalLength", fcoll);
        giraffe_model_set_parameter(self->model, "Magnification", gcam);
        giraffe_model_set_parameter(self->model, "Angle", theta);
        giraffe_model_set_parameter(self->model, "Spacing", grating->space);

        if (strcmp(name, "xoptmod2") == 0) {
            giraffe_model_set_parameter(self->model, "Sdx", sdx);
            giraffe_model_set_parameter(self->model, "Sdy", sdy);
            giraffe_model_set_parameter(self->model, "Sphi", sphi);
        }

        if (cpl_error_get_code() != CPL_ERROR_NONE) {
            giraffe_wlsolution_delete(self);
            return NULL;
        }

        giraffe_error_pop();


        /*
         * Get the wavelength residuals fit coefficients from the wavelength
         * solution table.
         */

        self->residuals = giraffe_wlresiduals_create(solution);

        if (self->residuals == NULL) {
            self->subslits = FALSE;
        }

    }

    return self;

}


void
giraffe_wlsolution_delete(GiWlSolution *self)
{

    if (self != NULL) {

        if (self->model != NULL) {
            giraffe_model_delete(self->model);
        }

        if (self->residuals != NULL) {
            giraffe_wlresiduals_delete(self->residuals);
        }

        cx_free(self);

    }

    return;

}


const cxchar *
giraffe_wlsolution_name(const GiWlSolution *self)
{

    GiModel *model = NULL;


    cx_assert(self != NULL);

    model = self->model;
    cx_assert(model != NULL);

    return giraffe_model_get_name(model);

}


GiModel *
giraffe_wlsolution_model(const GiWlSolution *self)
{

    cx_assert(self != NULL);

    return self->model;

}


cxint
giraffe_wlsolution_set_subslits(GiWlSolution *self, cxbool flag)
{

    cx_assert(self != NULL);

    if (self->residuals != NULL) {
        return 1;
    }

    self->subslits = flag;

    return 0;

}


cxbool
giraffe_wlsolution_get_subslits(const GiWlSolution *self)
{

    cx_assert(self != NULL);

    return self->subslits;

}


cxint
giraffe_wlsolution_set_residuals(GiWlSolution *self,
                                 const GiWlResiduals *residuals)
{

    cxbool subslits = FALSE;


    cx_assert(self != NULL);

    if (residuals == NULL) {
        return 1;
    }


    /* FIXME: This is a very weak check whether the residuals to accept
     *        are valid for the current subslits flag setting. A better
     *        mechanism needs to be put here, but this needs to be supported
     *        by the GiWlResidual class.
     */

    subslits = giraffe_wlresiduals_get(residuals, 0) == NULL;

    if (self->subslits != subslits) {
        return 2;
    }

    giraffe_wlsolution_reset_residuals(self);

    self->residuals = (GiWlResiduals *)residuals;

    return 0;

}


GiWlResiduals *
giraffe_wlsolution_get_residuals(const GiWlSolution *self)
{

    cx_assert(self != NULL);

    return self->residuals;

}


void
giraffe_wlsolution_reset_residuals(GiWlSolution *self)
{

    cx_assert(self != NULL);

    if (self->residuals != NULL) {
        giraffe_wlresiduals_delete(self->residuals);
        self->residuals = NULL;
    }

    return;

}


cxdouble
giraffe_wlsolution_compute_pixel(const GiWlSolution *self, cxdouble lambda,
                                 cxdouble x, cxdouble y, cxint *status)
{

    cxint code = 0;
    cxint _status = 0;

    cxdouble result = 0.;


    cx_assert(self != NULL);

    giraffe_error_push();

    giraffe_model_set_argument(self->model, "xf", x);
    giraffe_model_set_argument(self->model, "yf", y);
    giraffe_model_set_argument(self->model, "lambda", lambda);

    if (cpl_error_get_code() != CPL_ERROR_NONE) {

        if (status != NULL) {
            *status = -128;
        }

        return result;
    }

    giraffe_error_pop();

    code = giraffe_model_evaluate(self->model, &result, &_status);

    if (code != 0) {

        if (status != NULL) {
            *status = -128;
        }

        return result;

    }

    if (status != NULL) {
        *status = _status;
    }

    return result;

}


cxdouble
giraffe_wlsolution_compute_residual(const GiWlSolution *self, cxdouble x,
                                    cxdouble y)
{

    cxint i;

    cxdouble r = 0.;

    const GiWlResiduals *residuals = NULL;


    cx_assert(self != NULL);

    residuals = giraffe_wlsolution_get_residuals(self);

    if (residuals == NULL) {
        return r;
    }


    /*
     * Find first residual fit with matching ranges. This is used
     * for the residual computation assuming that the intervals
     * for which the fits are valid do not overlap.
     */

    for (i = 0; (cxsize)i < giraffe_wlresiduals_get_size(residuals); i++) {

        const GiChebyshev2D *fit = giraffe_wlresiduals_get(residuals, i);

        if (fit != NULL) {

            cxdouble ax;
            cxdouble bx;
            cxdouble ay;
            cxdouble by;

            giraffe_chebyshev2d_get_range(fit, &ax, &bx, &ay, &by);

            if (ax <= x && x <= bx && ay <= y && y <= by) {
                r = giraffe_chebyshev2d_eval(fit, x, y);
                break;
            }

        }

    }

    return r;

}


GiTable *
giraffe_wlsolution_create_table(const GiWlSolution *solution)
{


    cxint sign = 1;

    cxdouble value = 0.;

    cpl_propertylist *properties = NULL;

    GiTable *result = NULL;

    const GiModel *model = NULL;

    const GiWlResiduals *residuals = NULL;


    if (solution == NULL) {
        return NULL;
    }


    result = giraffe_table_new();
    cx_assert(result != NULL);

    properties = cpl_propertylist_new();
    cx_assert(properties != NULL);


    /*
     * Build up wavelength solution properties
     */

    cpl_propertylist_update_string(properties, GIALIAS_GIRFTYPE,
                                   "WLSOLUTION");
    cpl_propertylist_set_comment(properties, GIALIAS_GIRFTYPE,
                                 "Giraffe frame type.");

    cpl_propertylist_update_string(properties, GIALIAS_WSOL_OMNAME,
                                   giraffe_wlsolution_name(solution));
    cpl_propertylist_set_comment(properties, GIALIAS_WSOL_OMNAME,
                                 "Optical model name");

    model = giraffe_wlsolution_model(solution);

    sign = giraffe_model_get_parameter(model,"Orientation") < 0 ? -1 : 1;
    cpl_propertylist_update_int(properties, GIALIAS_WSOL_OMDIR, sign);
    cpl_propertylist_set_comment(properties, GIALIAS_WSOL_OMDIR,
                                 "Optical model orientation");

    value = giraffe_model_get_parameter(model, "FocalLength");
    cpl_propertylist_update_double(properties, GIALIAS_WSOL_OMFCOLL, value);
    cpl_propertylist_set_comment(properties, GIALIAS_WSOL_OMFCOLL,
                                 "Optical model focal length");

    value = giraffe_model_get_parameter(model, "Magnification");
    cpl_propertylist_update_double(properties, GIALIAS_WSOL_OMGCAM, value);
    cpl_propertylist_set_comment(properties, GIALIAS_WSOL_OMGCAM,
                                 "Optical model camera factor");

    value = giraffe_model_get_parameter(model, "Angle");
    cpl_propertylist_update_double(properties, GIALIAS_WSOL_OMGTHETA,
                                   value);
    cpl_propertylist_set_comment(properties, GIALIAS_WSOL_OMGTHETA,
                                 "Optical model grating angle");

    if (strcmp(giraffe_wlsolution_name(solution), "xoptmod2") == 0) {

        value = giraffe_model_get_parameter(model, "Sdx");
        cpl_propertylist_update_double(properties, GIALIAS_WSOL_OMSDX,
                                       value);
        cpl_propertylist_set_comment(properties, GIALIAS_WSOL_OMSDX,
                                     "Optical model slit x-offset");

        value = giraffe_model_get_parameter(model, "Sdy");
        cpl_propertylist_update_double(properties, GIALIAS_WSOL_OMSDY,
                                       value);
        cpl_propertylist_set_comment(properties, GIALIAS_WSOL_OMSDY,
                                     "Optical model slit y-offset");

        value = giraffe_model_get_parameter(model, "Sphi");
        cpl_propertylist_update_double(properties, GIALIAS_WSOL_OMSPHI,
                                       value);
        cpl_propertylist_set_comment(properties, GIALIAS_WSOL_OMSPHI,
                                     "Optical model slit rotation");

    }


    /*
     * Add optical model residuals fit coefficients, if they are present
     * in the wavelength solution.
     */

    residuals = giraffe_wlsolution_get_residuals(solution);

    if (residuals != NULL) {

        cpl_table *coeffs = giraffe_wlresiduals_table(residuals);

        if (coeffs != NULL) {
            giraffe_table_set(result, coeffs);
        }

    }

    giraffe_table_set_properties(result, properties);

    cpl_propertylist_delete(properties);
    properties = NULL;

    return result;

}
/**@}*/
