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

#include "gierror.h"
#include "gimessages.h"
#include "giframe.h"
#include "gifiberutils.h"
#include "gimatrix.h"
#include "gislitgeometry.h"


/**
 * @defgroup gislitgeometry Slit Geometry
 *
 * TBD
 */

/**@{*/

/*
 * @brief
 *   Create a cpl_matrix at a specified position of the GiSlitGeometry
 *
 * @param self  GiSlitGeometry in which to create the cpl_matrix
 * @param pos   Position at which to create the matrix
 * @param nrow  Number of rows of the matrix
 * @param ncol  Number of columns of the matrix
 *
 * Creates a cpl_matrix at position @em pos inside the GiSlitGeometry
 * @em self of @em nrow rows and @em ncol columns. If a cpl_matrix is
 * already present at position @em pos, it is properly deallocated first.
 */

inline static void
_giraffe_slitgeometry_insert(GiSlitGeometry *self, cxint pos, cxint nrow,
                             cxint ncol)
{

    if (self == NULL) {
        return;
    }

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

    if ((pos < 0) || (pos > self->nsubslits)) {
        return;
    }

    if (self->subslits[pos] != NULL) {
        cpl_matrix_delete(self->subslits[pos]);
    }

    self->subslits[pos] = cpl_matrix_new(nrow, ncol);

    return;

}


/**
 * @brief
 *   Create a new GiSlitGeometry
 *
 * @return
 *  Pointer to newly created @em GiSlitGeometry or @c NULL if an
 *  error occured
 *
 * Creates a new slit geometry setup object.
 */

GiSlitGeometry *
giraffe_slitgeometry_new(void)
{

    GiSlitGeometry *self = cx_malloc(sizeof *self);

    self->xf = NULL;
    self->yf = NULL;

    self->nsubslits = 0;
    self->subslits  = NULL;

    return self;

}


/**
 * @brief
 *   Creates a (deep) copy of the GiSlitGeometry
 *
 * @param other  GiSlitGeometry to copy
 *
 * @return Pointer to new GiSlitGeometry or NULL if an error occured
 *
 * Returns a pointer to a (deep)copy of the input GiSlitGeometry
 * @em other.
 */

GiSlitGeometry *
giraffe_slitgeometry_duplicate(GiSlitGeometry *other)
{

    GiSlitGeometry *clone = NULL;


    if (other) {

        cxint i;


        clone = (GiSlitGeometry *)cx_malloc(sizeof *clone);

        if ((other->subslits == NULL) || (other->nsubslits == 0)) {

            clone->nsubslits = other->nsubslits;
            clone->subslits = other->subslits;

            return clone;

        }

        clone->nsubslits = other->nsubslits;
        clone->subslits  = cx_calloc(clone->nsubslits, sizeof(cpl_matrix *));

        for (i = 0; i < other->nsubslits; i++) {
            giraffe_slitgeometry_set(clone, i,
                                     giraffe_slitgeometry_get(other,i));
        }

    }

    return clone;

}


/**
 * @brief
 *   Create a slit geometry object from a slit geometry table.
 *
 * @param slitgeometry  Slit geometry table.
 * @param subslits      Flag controlling the use of multiple subslits.
 *
 * @return
 *  The function returns the newly created slit geometry, or @c NULL if
 *  an error occurred.
 *
 * The function creates a slit geometry object from the slit geometry
 * table @em slitgeometry. If @em subslits is @c 0 all fibers are put
 * into a single subslit. If @em subslits is different from @c 0, multiple
 * subslits are created according to the subslit information found in
 * @em slitgeometry, and each fiber is put into the corresponding subslit.
 *
 * @see GiSlitGeometry
 */

GiSlitGeometry *
giraffe_slitgeometry_create(GiTable *slitgeometry, cxbool subslits)
{

    const cxchar *fctid = "giraffe_slitgeometry_create";


    const cxchar *c_xf = "XF";
    const cxchar *c_yf = "YF";
    const cxchar *c_fps = "FPS";
    const cxchar *c_ssn = "SSN";
    const cxchar *c_rindex = NULL;

    /*cxint status;*/
    cxint nfibers = 0;
    cxint max_nsubslits = 0;
    cxint i             = 0;
    cxint j             = 0;
    cxint count         = 0;
    cxint column_index  = 0;

    cpl_matrix *nsubslits = NULL;

    cpl_table *_slitgeometry = NULL;

    GiSlitGeometry *self = NULL;


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

    self = giraffe_slitgeometry_new();

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

    _slitgeometry = giraffe_table_get(slitgeometry);
    nfibers = cpl_table_get_nrow(_slitgeometry);

    self->xf = cpl_matrix_new(nfibers, 1);
    self->yf = cpl_matrix_new(nfibers, 1);
    self->fps = cpl_matrix_new(nfibers, 1);
    self->rindex = cpl_matrix_new(nfibers, 1);

    nsubslits = cpl_matrix_new(nfibers, 1);


    c_rindex = giraffe_fiberlist_query_index(_slitgeometry);

    /*
     *  Copy relevant data to matrices
     */

    max_nsubslits = 0;

    for (i = 0; i < nfibers; i++) {

        cxint _nsubslits = cpl_table_get_int(_slitgeometry, c_ssn, i, NULL);
        cxint _fps = cpl_table_get_int(_slitgeometry, c_fps, i, NULL) - 1;
        cxint _index = cpl_table_get_int(_slitgeometry, c_rindex,
                                         i, NULL) - 1;

        cxdouble _xf = cpl_table_get(_slitgeometry, c_xf, i, NULL);;
        cxdouble _yf = cpl_table_get(_slitgeometry, c_yf, i, NULL);


        if (_nsubslits > max_nsubslits) {
            max_nsubslits = _nsubslits;
        }

        /*status =*/ cpl_matrix_set(self->xf, i, 0, _xf);
        /*status =*/ cpl_matrix_set(self->yf, i, 0, _yf);
        /*status =*/ cpl_matrix_set(self->fps, i, 0, (cxdouble)_fps);
        /*status =*/ cpl_matrix_set(self->rindex, i, 0, (cxdouble)_index);

        /*status =*/ cpl_matrix_set(nsubslits, i, 0, (cxdouble)_nsubslits);

    }


    /*
     *  Create Slit Geometry
     */

    if (subslits) {

        /* create multiple subslits */

        giraffe_slitgeometry_resize(self, max_nsubslits);

        for (i = 1; i <= max_nsubslits; i++) {

            cxint curr_ssn;

            cpl_matrix *ref_matrix = NULL;

            count = 0;
            for (j = 0; j < nfibers; j++) {
                curr_ssn = (cxint) cpl_matrix_get(nsubslits, j, 0);

                if (i == curr_ssn) {
                    ++count;
                }
            }

            _giraffe_slitgeometry_insert(self, i - 1, count, 1);

            ref_matrix = giraffe_slitgeometry_get(self, i - 1);

            column_index = 0;
            for (j = 0; j < nfibers; j++) {

                curr_ssn = (cxint) cpl_matrix_get(nsubslits, j, 0);

                if (i == curr_ssn) {

                    /*status =*/ cpl_matrix_set(ref_matrix, column_index, 0,
                                            (cxdouble)j);
                    ++column_index;
                }
            }
        }

        cpl_msg_debug(fctid, "Using multiple slits for Slit Geometry");

    }
    else {

        /* create one subslit containing all fibers */

        cpl_matrix *ref_matrix = NULL;

        giraffe_slitgeometry_resize(self, 1);
        _giraffe_slitgeometry_insert(self, 0, nfibers, 1);

        ref_matrix = giraffe_slitgeometry_get(self, 0);

        for (j = 0; j < nfibers; j++) {

            /*status =*/ cpl_matrix_set(ref_matrix, j, 0, (cxdouble)j);

        }

        cpl_msg_debug(fctid, "Using single slit for Slit Geometry");

    }

    cpl_matrix_delete(nsubslits);

    return self;

}


/**
 * @brief
 *   Destroy an @em GiSlitGeometry
 *
 * @param self  GiSlitGeometry to destroy
 *
 * @return Nothing.
 *
 * Destroys an GiSlitGeometry and frees all associated memory
 */

void
giraffe_slitgeometry_delete(GiSlitGeometry *self)
{

    if (self == NULL) {
        return;
    }

    if (self->subslits != NULL) {

        cxint i;

        for (i = 0; i < self->nsubslits; i++) {
            cpl_matrix_delete(self->subslits[i]);
        }

        cx_free(self->subslits);

    }

    return;

}


/**
 * @brief
 *   Returns current size of a GiSlitGeometry
 *
 * @param self  GiSlitGeometry for which to return the size
 *
 * Returns the current size of the GiSlitGeometry i.e. how
 * many matrices can be stored in it/are stored in it.
 *
 * @return Maximum number of cpl_matrix to store in GiSlitGeometry
 *         or <0 if an error occured
 */

cxint
giraffe_slitgeometry_size(GiSlitGeometry *self)
{

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

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

    return 0;

}


/**
 * @brief
 *   Destructive resize of a GiSlitGeometry
 *
 * @param self  GiSlitGeometry which to destory
 * @param size  New size
 *
 * Resizes the GiSlitGeometry @a self by first deallocating the old
 * informatin contained in it. Afterwards new memory is allocated for the
 * size given
 */

void
giraffe_slitgeometry_resize(GiSlitGeometry *self, cxint size)
{

    if (self == NULL) {
        return;
    }

    if (size == self->nsubslits) {
        return;
    }

    if (self->subslits != NULL) {

        cxint i;

        for (i = 0; i < self->nsubslits; i++) {
            cpl_matrix_delete(self->subslits[i]);
        }

    }

    cx_free(self->subslits);

    self->nsubslits = size;
    self->subslits = cx_calloc(self->nsubslits, sizeof(cpl_matrix *));

    return;

}


/**
 * @brief
 *   Sets (copies) a cpl_matrix to a specified position of the GiSlitGeometry
 *
 * @param self  GiSlitGeometry to copy matrix into
 * @param pos   Position to which to copy the matrix
 * @param nm    Matrix to copy
 *
 * Copies a cpl_matrix @a nm to position @em pos inside the GiSlitGeometry
 * @em self. If a cpl_matrix is already present at position @em pos,
 * it is properly deallocated first. @a nm can be NULL, in that case the
 * matrix at position @a pos is deleted and the position is marked as being
 * empty.
 */

void
giraffe_slitgeometry_set(GiSlitGeometry *self, cxint pos,
                         cpl_matrix *nm)
{

    if (self == NULL) {
        return;
    }

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

    if ((pos < 0) || (pos > self->nsubslits)) {
        return;
    }

    if (self->subslits[pos] != NULL) {
        cpl_matrix_delete(self->subslits[pos]);
    }

    if (nm) {
        self->subslits[pos] = cpl_matrix_duplicate(nm);
    }
    else {
        self->subslits[pos] = NULL;
    }

    return;

}


/**
 * @brief
 *   Gets a reference to the matrix at a specified position
 *
 * @param self  GiSlitGeometry from which to retrieve the reference
 * @param pos   Position from which to retrieve it
 *
 * @return Pointer to @em cpl_matrix or NULL if an error occured
 *
 * Returns a reference i.e. a pointer to a @em cpl_matrix contained in
 * the GiSlitGeometry @em self at position @em pos.
 */

cpl_matrix *
giraffe_slitgeometry_get(GiSlitGeometry *self, cxint pos)
{

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

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

    if ((pos < 0) || (pos > self->nsubslits)) {
        return NULL;
    }

    return self->subslits[pos];

}


/**
 * @brief
 *   Dump the the information contained in a GiSlitGeometry to output
 *
 * @param self  GiSlitGeometry to dump
 *
 * Dump the information contained in a GiSlitGeometry @a self using the
 * CPL messaging subsystem.
 */

void
giraffe_slitgeometry_print(GiSlitGeometry *self)
{

    const cxchar *fctid = "giraffe_slitgeometry_print";

    cxint i;

    gi_message("Current slit geometry setup");

    if (self == NULL) {
        gi_message("Empty slit geometry!");
        return;
    }

    if (self->subslits == NULL) {
        gi_message(fctid, "Invalid slit geometry, no slit matrices "
                   "present!");
        return;
    }


    for (i = 0; i < self->nsubslits; i++) {

        cxint nrow = 0;

        cpl_matrix *ref  = NULL;

        ref  = giraffe_slitgeometry_get(self, i);
        nrow = cpl_matrix_get_nrow(ref);

        giraffe_matrix_dump(ref, nrow);
    }

    return;

}


/**
 * @brief
 *   Load the slit geometry information for a given fiber setup.
 *
 * @param fibers    Table of fibers for which the slit geometry is needed.
 * @param filename  The name of the file containing the slit geometry.
 * @param pos       The data set index of the slit geometry within the file.
 * @param tag       The data set name of the slit geometry.
 *
 * @return The function returns a table containing the slit geometry for the
 *   given fiber setup if no error occurred, or @c NULL otherwise.
 *
 * The function creates the slit geometry table for the fiber setup specified
 * by the fiber table @em fibers. The slit geometry information is read from
 * the data set @em pos of the file @em filename. If the data set
 * identifier @em tag is not @c NULL, it is used to validate the data set
 * with the number @em pos to contain a valid slit geometry. In this case
 * @em tag must match the name of the data set @em pos.
 */

GiTable *
giraffe_slitgeometry_load(const GiTable *fibers, const cxchar *filename,
                          cxint pos, const cxchar *tag)
{

    const cxchar *fctid = "giraffe_slitgeometry_load";


    const cxchar *fps_name = "FPS";
    const cxchar *ridx = NULL;

    cxint i;
    cxint nfibers = 0;

    cpl_propertylist *properties = NULL;

    cpl_table *_fibers = NULL;
    cpl_table *_slit_geometry = NULL;

    GiTable *slit_geometry = NULL;

    GiInstrumentMode mode;



    if (fibers == NULL) {
        cpl_error_set(fctid, CPL_ERROR_NULL_INPUT);
        return NULL;
    }

    _fibers = giraffe_table_get(fibers);

    if (_fibers == NULL) {
        cpl_error_set(fctid, CPL_ERROR_DATA_NOT_FOUND);
        return NULL;
    }

    /*
     * Get the instrument setup corresponding to the slit geometry in
     * the file.
     */

    properties = cpl_propertylist_load(filename, 0);

    if (properties == NULL) {
        cpl_msg_error(fctid, "Cannot load properies of data set 0 "
                      "from `%s'!", filename);
        cpl_propertylist_delete(properties);
        return NULL;
    }
    else {

        mode = giraffe_get_mode(properties);

        if (mode == GIMODE_NONE) {
            cpl_msg_error(fctid, "Invalid instrument mode!");

            cpl_propertylist_delete(properties);
            return NULL;
        }

    }

    cpl_propertylist_delete(properties);


    /*
     * Load the slit geometry information
     */

    slit_geometry = giraffe_table_new();

    giraffe_error_push();

    if (giraffe_table_load(slit_geometry, filename, pos, tag)) {
        if (cpl_error_get_code() == CPL_ERROR_BAD_FILE_FORMAT) {
            cpl_msg_error(fctid, "Data set %d in `%s' is not a slit "
                          "geometry table!", pos, filename);
            giraffe_table_delete(slit_geometry);
            return NULL;
        }
        else {
            cpl_msg_error(fctid, "Cannot load data set %d (slit geometry) "
                          "from `%s!", pos, filename);
            giraffe_table_delete(slit_geometry);
            return NULL;
        }
    }

    giraffe_error_pop();

    _slit_geometry = giraffe_table_get(slit_geometry);

    if (!cpl_table_has_column(_slit_geometry, fps_name)) {
        if (cpl_table_has_column(_slit_geometry, "NSPEC")) {
            cpl_msg_warning(fctid, "Slit geometry loaded from `%s' uses "
                            "deprecated OGL column names.", filename);

            // FIXME: This should not be done here. The data should
            //        be correct!
            //        The following code assumes that all possible fibers
            //        are listed in the order they appear on an image.
            //        Only fibers which do not appear on the CCD on the
            //        right side (high FPS numbers for Medusa and IFU, low
            //        FPS numbers for Argus) may be missing. For all other
            //        cases the code below would just generate garbage.

            cpl_table_duplicate_column(_slit_geometry, fps_name,
                                       _slit_geometry, "NSPEC");

            cpl_table_name_column(_slit_geometry, "NSPEC", "INDEX");

            if (mode == GIMODE_ARGUS) {

                cxint nrow = cpl_table_get_nrow(_slit_geometry);

                for (i = 0; i < nrow; i++) {

                    cxint idx = cpl_table_get_int(_slit_geometry, "INDEX",
                                                  nrow - i - 1, NULL);

                    cpl_table_set_int(_slit_geometry, fps_name, i, idx);

                }

            }

        }
        else {
            cpl_error_set(fctid, CPL_ERROR_DATA_NOT_FOUND);
            giraffe_table_delete(slit_geometry);
            return NULL;
        }
    }


    /*
     * Extract the slit geometry information corresponding to the
     * given fiber setup
     */

    nfibers = cpl_table_get_nrow(_fibers);

    cpl_table_unselect_all(_slit_geometry);

    for (i = 0; i < cpl_table_get_nrow(_slit_geometry); i++) {

        cxint fps = cpl_table_get_int(_slit_geometry, fps_name, i, NULL);
        cxint j;

        for (j = 0; j < nfibers; j++) {

            cxint _fps = cpl_table_get_int(_fibers, "FPS", j, NULL);

            if (_fps == fps) {
                cpl_table_select_row(_slit_geometry, i);
                break;
            }

        }

    }

    _slit_geometry = cpl_table_extract_selected(_slit_geometry);


    ridx = giraffe_fiberlist_query_index(_fibers);
    cpl_table_new_column(_slit_geometry, "RINDEX", CPL_TYPE_INT);

    for (i = 0; i < cpl_table_get_nrow(_slit_geometry); i++) {

        cxint fps = cpl_table_get_int(_slit_geometry, fps_name, i, NULL);
        cxint j;

        for (j = 0; j < nfibers; j++) {

            cxint _fps = cpl_table_get_int(_fibers, "FPS", j, NULL);

            if (_fps == fps) {

                cxint _ridx = cpl_table_get_int(_fibers, ridx, j , NULL);

                cpl_table_set_int(_slit_geometry, "RINDEX", i, _ridx);
                break;

            }

        }

    }


    /*
     * If the loaded table had the OGL label for the fiber position within
     * the slit (FPS) change it.
     */

    if (strcmp(fps_name, "FPS") != 0) {
        cpl_table_name_column(_slit_geometry, fps_name, "FPS");
    }


    /*
     * Reset the index column
     */

    for (i = 0; i < cpl_table_get_nrow(_slit_geometry); i++) {
        cpl_table_set_int(_slit_geometry, "INDEX", i, i + 1);
    }

    giraffe_table_set(slit_geometry, _slit_geometry);

    cpl_table_delete(_slit_geometry);
    _slit_geometry = NULL;

    return slit_geometry;

}


cpl_frame *
giraffe_slitgeometry_save(const GiTable *slitgeometry)
{

    cpl_frame *frame = NULL;


    if (slitgeometry) {

        GiTable *slit = giraffe_table_duplicate(slitgeometry);


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

        if (cpl_table_has_column(giraffe_table_get(slit), "RINDEX")) {
            cpl_table_erase_column(giraffe_table_get(slit), "RINDEX");
        }

        frame = giraffe_frame_create_table(slit, GIFRAME_SLITSETUP,
                                           CPL_FRAME_LEVEL_FINAL,
                                           TRUE, TRUE);

        giraffe_table_delete(slit);

    }

    return frame;

}
/**@}*/
