/*
 * 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 <cxstring.h>
#include <cxslist.h>
#include <cxstrutils.h>
#include <cxmessages.h>

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

#include "gialias.h"
#include "gierror.h"
#include "giframe.h"
#include "gitable.h"
#include "gimessages.h"
#include "giutils.h"
#include "gifiberutils.h"


/**
 * @defgroup gifiberutils Fiber Positioner Utilities
 *
 * TBD
 */

/**@{*/

inline static cxint
_giraffe_compare_int(cxcptr first, cxcptr second)
{

    cxint *_first = (cxint *)first;
    cxint *_second = (cxint *)second;

    return *_first - *_second;

}


/**
 * @brief
 *   Creates the fiber table
 *
 * @param filename  The file for which the fiber table is created.
 * @param nspec     Number of spectra to process.
 * @param spectra   List of spectrum indices to be used for the fiber table.
 *
 * @return On success the function returns the created fiber table, or
 *   @c NULL otherwise
 *
 * The function builds a table of fibers which have to be processed from
 * the slit geometry file @em slitfile and an OzPoz table if it is present
 * in the input file @em filename. If @em spectra is not @c NULL only the
 * spectra listed in @em spectra are used to build the fiber table.
 *
 * @warning The option to use a list of spectra for building the fiber table
 *   is not yet implemented.
 */

cpl_table *
giraffe_fiberlist_create(const cxchar *filename, cxint nspec,
                         const cxint *spectra)
{

    const cxchar *const fctid = "giraffe_fiberlist_create";


    cxbool calsim;

    cxint i;
    cxint status = 0;

    cxint nfibers;
    cxint nbuttons;

    cx_string *slit_name = NULL;

    cpl_table *fibers = NULL;
    cpl_table *_slits;
    cpl_table *_ozpoz;

    cpl_propertylist *properties = NULL;
    cpl_propertylist *sorting_order = NULL;

    GiTable *slits = NULL;
    GiTable *ozpoz = NULL;

    GiInstrumentMode mode;



    if (!filename) {
        return NULL;
    }


    /*
     * Check whether the input file is a calibration and retrieve the
     * name of the slit in use.
     */

    properties = cpl_propertylist_load(filename, 0);

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

        if (!cpl_propertylist_has(properties, GIALIAS_STSCFF) &&
            !cpl_propertylist_has(properties, GIALIAS_STSCTAL)) {
            cpl_msg_warning(fctid, "%s: Properties (%s, %s) not found! "
                            "Simultaneous calibration lamps assumed to "
                            "be off!", filename, GIALIAS_STSCFF,
                            GIALIAS_STSCTAL);
            calsim = FALSE;
        }
        else {

            cxint scff = cpl_propertylist_get_bool(properties,
                                                   GIALIAS_STSCFF);
            cxint sctal= cpl_propertylist_get_bool(properties,
                                                   GIALIAS_STSCTAL);


            if (scff || sctal) {
                cpl_msg_info(fctid, "Simultaneous calibration lamps "
                             "are on.");
                calsim = TRUE;
            }
            else {
                cpl_msg_info(fctid, "Simultaneous calibration lamps "
                             "are off.");
                calsim = FALSE;
            }

        }


        slit_name =
            cx_string_create(cpl_propertylist_get_string(properties,
                                                         GIALIAS_SLITNAME));
        if (!slit_name) {
            cpl_msg_error(fctid, "%s: Property (%s) not found!", filename,
                          GIALIAS_SLITNAME);
            cpl_propertylist_delete(properties);
            return NULL;
        }
        else {
            cx_string_strip(slit_name);
        }

        mode = giraffe_get_mode(properties);

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

            cx_string_delete(slit_name);
            cpl_propertylist_delete(properties);

            return NULL;
        }

        cpl_propertylist_delete(properties);
    }


    /*
     * Load OzPoz table from current frame
     */

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

    giraffe_error_push();

    status = giraffe_table_load(ozpoz, filename, GIOZPOZ_EXTENSION,
                                GIOZPOZ_MAGIC);

    if (status) {
        if (cpl_error_get_code() == CPL_ERROR_BAD_FILE_FORMAT) {
            cpl_msg_error(fctid, "Data set %d in `%s' is not an "
                          "OzPoz table!", GIOZPOZ_EXTENSION, filename);
            giraffe_table_delete(ozpoz);
            cpl_table_delete(fibers);

            return NULL;
        }
        else {
            if (status != 2) {
                cpl_msg_error(fctid, "No OzPoz table found in `%s'!",
                              filename);
                giraffe_table_delete(ozpoz);
                return NULL;
            }

            cpl_msg_warning(fctid, "Empty OzPoz table found in `%s'.",
                            filename);

        }
    }

    giraffe_error_pop();

    _ozpoz = giraffe_table_get(ozpoz);


    /*
     * Load fiber table from current frame.
     */

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

    giraffe_error_push();

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

    giraffe_error_pop();

    _slits = giraffe_table_get(slits);


    /*
     * Select all entries with the appropriate slit name from the table
     */

    cpl_table_select_all(_slits);
    cpl_table_and_selected_string(_slits, "Slit", CPL_NOT_EQUAL_TO,
                                  cx_string_get(slit_name));

    giraffe_error_push();

    cpl_table_erase_selected(_slits);

    if (cpl_error_get_code() != CPL_ERROR_NONE) {
        cpl_msg_error(fctid, "Invalid slit `%s' selected. No fibers found.",
                      cx_string_get(slit_name));

        cx_string_delete(slit_name);
        slit_name = NULL;

        giraffe_table_delete(slits);
        slits = NULL;

        return NULL;
    }

    giraffe_error_pop();

    cx_string_delete(slit_name);
    slit_name = NULL;


    /*
     * Move the relevant columns from the fiber table to the
     * final fibers list.
     */

    nfibers = cpl_table_get_nrow(_slits);
    fibers = cpl_table_new(nfibers);

    giraffe_error_push();

    cpl_table_new_column(fibers, "INDEX", CPL_TYPE_INT);
    cpl_table_new_column(fibers, "FPS", CPL_TYPE_INT);
    cpl_table_new_column(fibers, "SSN", CPL_TYPE_INT);
    cpl_table_new_column(fibers, "PSSN", CPL_TYPE_INT);
    cpl_table_new_column(fibers, "RP", CPL_TYPE_INT);

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

        cxchar *s;

        cxint fps = strtol(cpl_table_get_string(_slits, "FPS", i), NULL, 10);
        cxint ssn = strtol(cpl_table_get_string(_slits, "SSN", i), NULL, 10);
        cxint pssn = strtol(cpl_table_get_string(_slits, "PSSN", i),
                            NULL, 10);
        cxint rp = -1;


        s = (cxchar*) cpl_table_get_string(_slits, "RP", i);

        if (s != NULL) {
            rp = strtol(s, NULL, 10);
        }
        else {
            if (mode == GIMODE_ARGUS) {

                const cxchar *rpid = cpl_table_get_string(_slits,
                                                          "Retractor", i);

                if (cx_strncasecmp(rpid, "Cal", 3) != 0) {
                    rp = 0;
                }

            }
        }

        cpl_table_set_int(fibers, "FPS", i, fps);
        cpl_table_set_int(fibers, "SSN", i, ssn);
        cpl_table_set_int(fibers, "PSSN", i, pssn);
        cpl_table_set_int(fibers, "RP", i, rp);

    }

    if (mode == GIMODE_IFU || mode == GIMODE_ARGUS) {

        if (cpl_table_has_column(_slits, "X") &&
            cpl_table_has_column(_slits, "Y")) {

            cpl_table_new_column(fibers, "X", CPL_TYPE_INT);
            cpl_table_new_column(fibers, "Y", CPL_TYPE_INT);

            for (i = 0; i < nfibers; i++) {
                const cxchar *s;

                cxint x = 0;
                cxint y = 0;


                s = cpl_table_get_string(_slits, "X", i);

                if (s != NULL) {
                    x = strtol(s, NULL, 10);
                }

                s = cpl_table_get_string(_slits, "Y", i);

                if (s != NULL) {
                    y = strtol(s, NULL, 10);
                }

                cpl_table_set_int(fibers, "X", i, x);
                cpl_table_set_int(fibers, "Y", i, y);
            }

        }

    }

    cpl_table_move_column(fibers, "Retractor", _slits);

    if (cpl_error_get_code() != CPL_ERROR_NONE) {
        cpl_msg_error(fctid, "Data set %d in `%s' is not a valid "
                      "fiber table!", GIFIBER_EXTENSION, filename);
        giraffe_table_delete(slits);
        cpl_table_delete(fibers);

        return NULL;
    }

    giraffe_error_pop();

    giraffe_table_delete(slits);


    /*
     * For Argus the slit is mirrored compared to IFU and Medusa
     * and we have to reverse the order of the slits table.
     */

    if (mode == GIMODE_ARGUS) {

        sorting_order = cpl_propertylist_new();

        cpl_propertylist_append_bool(sorting_order, "FPS", 1);
        cpl_table_sort(fibers, sorting_order);

        cpl_propertylist_delete(sorting_order);
        sorting_order = NULL;

    }


    /*
     * Postprocess initial fiber table
     */

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

        const cxchar *s = cpl_table_get_string(fibers, "Retractor", i);


        if (strstr(s, "Calibration")) {
            cpl_table_set_int(fibers, "RP", i, -1);
        }

        cpl_table_set_int(fibers, "INDEX", i, i + 1);

    }

    if (!cpl_table_has_column(fibers, "FPD")) {
        cpl_table_duplicate_column(fibers, "FPD", fibers, "INDEX");
    }

    cpl_table_new_column(fibers, "OBJECT", CPL_TYPE_STRING);
    cpl_table_new_column(fibers, "R", CPL_TYPE_DOUBLE);
    cpl_table_new_column(fibers, "THETA", CPL_TYPE_DOUBLE);
    cpl_table_new_column(fibers, "ORIENT", CPL_TYPE_DOUBLE);
    cpl_table_new_column(fibers, "TYPE", CPL_TYPE_STRING);

    cpl_table_fill_column_window_double(fibers, "R", 0, nfibers, 0.);
    cpl_table_fill_column_window_double(fibers, "THETA", 0, nfibers, 0.);
    cpl_table_fill_column_window_double(fibers, "ORIENT", 0, nfibers, 0.);

    if (_ozpoz != NULL) {
        if (cpl_table_has_column(_ozpoz, "RA")) {
            cpl_table_new_column(fibers, "RA", CPL_TYPE_DOUBLE);
            cpl_table_fill_column_window_double(fibers, "RA", 0,
                                                nfibers, 0.);
        }

        if (cpl_table_has_column(_ozpoz, "DEC")) {
            cpl_table_new_column(fibers, "DEC", CPL_TYPE_DOUBLE);
            cpl_table_fill_column_window_double(fibers, "DEC", 0,
                                                nfibers, 0.);
        }

        if (cpl_table_has_column(_ozpoz, "MAGNITUDE")) {
            cpl_table_new_column(fibers, "MAGNITUDE", CPL_TYPE_DOUBLE);
            cpl_table_fill_column_window_double(fibers, "MAGNITUDE", 0,
                                                nfibers, 0.);
        }

        if (cpl_table_has_column(_ozpoz, "B_V")) {
            cpl_table_new_column(fibers, "B_V", CPL_TYPE_DOUBLE);
            cpl_table_fill_column_window_double(fibers, "B_V", 0,
                                                nfibers, 0.);
        }
    }


    /*
     * Select rows in the fiber table which have a corresponding entry
     * in the OzPoz table. Both tables are associated using the
     * `button number'. For matching entries the OzPoz data is copied
     * to the fiber table. Also the simultaneous calibration fibers
     * are copied.
     */

    nbuttons = _ozpoz == NULL ? 0 : cpl_table_get_nrow(_ozpoz);

    cpl_table_select_all(fibers);

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

        cxbool missing = TRUE;

        cxint fiber = cpl_table_get_int(fibers, "RP", i, NULL);


        /*
         * If fiber equals -1 it is a simultaneous calibration, which
         * has no entry in the OzPoz table. Otherwise we try to find it
         * in the OzPoz table and copy the data for this fiber.
         */

        if (fiber == -1 && calsim == TRUE) {
            cpl_table_set_string(fibers, "OBJECT", i, "CALSIM");
            cpl_table_unselect_row(fibers, i);
            missing = FALSE;
        }
        else if (fiber == 0 && mode == GIMODE_ARGUS) {
            cpl_table_unselect_row(fibers, i);
            missing = FALSE;
        }
        else {

            register cxint j;


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

                cxint button = cpl_table_get_int(_ozpoz, "BUTTON", j, NULL);


                if (fiber == button) {
                    const cxchar *object;
                    const cxchar *otype;

                    cxdouble r, theta, orient;

                    cxdouble ra = 0.;
                    cxdouble dec = 0.;
                    cxdouble mag = 0.;
                    cxdouble b_v = 0.;


                    object = cpl_table_get_string(_ozpoz, "OBJECT", j);
                    otype = cpl_table_get_string(_ozpoz, "TYPE", j);

                    r = cpl_table_get_double(_ozpoz, "R", j, NULL);
                    theta = cpl_table_get_double(_ozpoz, "THETA", j, NULL);
                    orient = cpl_table_get_double(_ozpoz, "ORIENT", j, NULL);

                    if (cpl_table_has_column(_ozpoz, "RA")) {
                        ra = cpl_table_get_double(_ozpoz, "RA", j, NULL);
                    }

                    if (cpl_table_has_column(_ozpoz, "DEC")) {
                        dec = cpl_table_get_double(_ozpoz, "DEC", j, NULL);
                    }

                    if (cpl_table_has_column(_ozpoz, "MAGNITUDE")) {
                        mag = cpl_table_get_float(_ozpoz, "MAGNITUDE", j,
                                                  NULL);
                    }

                    if (cpl_table_has_column(_ozpoz, "B_V")) {
                        b_v = cpl_table_get_double(_ozpoz, "B_V", j, NULL);
                        b_v = CX_CLAMP(b_v, -5., 5.);
                    }

                    cpl_table_set_string(fibers, "OBJECT", i, object);
                    cpl_table_set_string(fibers, "TYPE", i, otype);

                    cpl_table_set_double(fibers, "R", i, r);
                    cpl_table_set_double(fibers, "THETA", i, theta);
                    cpl_table_set_double(fibers, "ORIENT", i, orient);

                    if (cpl_table_has_column(fibers, "RA")) {
                        cpl_table_set_double(fibers, "RA", i, ra);
                    }

                    if (cpl_table_has_column(fibers, "DEC")) {
                        cpl_table_set_double(fibers, "DEC", i, dec);
                    }

                    if (cpl_table_has_column(fibers, "MAGNITUDE")) {
                        cpl_table_set_double(fibers, "MAGNITUDE", i, mag);
                    }

                    if (cpl_table_has_column(fibers, "B_V")) {
                        cpl_table_set_double(fibers, "B_V", i, b_v);
                    }

                    cpl_table_unselect_row(fibers, i);
                    missing = FALSE;
                    break;
                }
            }
        }

        if (missing == TRUE) {

            cxint _fps = cpl_table_get_int(fibers, "FPS", i, NULL);
            cpl_msg_debug(fctid, "Fiber at FPS = %d is not used", _fps);

        }

    }


    giraffe_error_push();

    cpl_table_erase_selected(fibers);

    if (cpl_error_get_code() != CPL_ERROR_NONE) {
        cpl_table_delete(fibers);
        return NULL;
    }

    giraffe_error_pop();

    giraffe_table_delete(ozpoz);
    ozpoz = NULL;


    /*
     * Finalize the fiber list by applying the user specified fiber
     * selection list. Fibers which do not have an entry in the
     * OzPoz table or which are beyond the limits are ignored.
     */

    if (spectra && nspec > 0) {

        register cxint rows = cpl_table_get_nrow(fibers);


        cx_assert(cpl_table_get_column_type(fibers, "FPD") == CPL_TYPE_INT);

        cpl_table_select_all(fibers);

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

            register cxint j;
            register cxint selected = 0;
            register cxint idx = cpl_table_get_int(fibers, "FPD", i, NULL);


            for (j = 0; j < nspec; j++) {
                if (idx == spectra[j]) {
                    selected = 1;
                    break;
                }
            }

            if (selected) {
                cpl_table_unselect_row(fibers, i);
            }
            else {
                cpl_table_select_row(fibers, i);
            }

        }

        giraffe_error_push();

        cpl_table_erase_selected(fibers);

        if (cpl_error_get_code() != CPL_ERROR_NONE) {
            cpl_table_delete(fibers);
            return NULL;
        }

        giraffe_error_pop();

    }


    /*
     * Update index column
     */

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


    /*
     * Sort the final table according to the INDEX column
     */

    cx_assert(sorting_order == NULL);

    sorting_order = cpl_propertylist_new();
    cpl_propertylist_append_bool(sorting_order, "INDEX", 0);

    cpl_table_sort(fibers, sorting_order);

    cpl_propertylist_delete(sorting_order);
    sorting_order = NULL;


    return fibers;

}


/**
 * @brief
 *   Load a fiber table from a file.
 *
 * @param filename  The name of the file.
 * @param dataset   The data set index of the fiber table within the file.
 * @param tag       The data set name of the fiber table.
 *
 * @return The function returns a table containing the fiber setup if no
 *   error occurred, or @c NULL otherwise.
 *
 * The function reads the fiber table from the data set @em dataset 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 dataset to contain
 * a fiber setup. In this case @em tag must match the data set name to be
 * considered as a valid fiber setup.
 *
 * The fiber tables which can be loaded using this function have to be
 * created with giraffe_fiberlist_create().
 *
 * @see giraffe_fiberlist_create()
 */

GiTable *
giraffe_fiberlist_load(const cxchar *filename, cxint dataset,
                       const cxchar *tag)
{

    const cxchar *fctid = "giraffe_fiberlist_load";


    GiTable *fibers = giraffe_table_new();


    cx_assert(fibers != NULL);

    giraffe_error_push();

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

    giraffe_error_pop();

    return fibers;

}


/**
 * @brief
 *   Save a fiber table to a file.
 *
 * @param fibers    The fiber table to save
 * @param filename  The name of the file.
 *
 * @return The function returns 0 on success or 1 otherwise. If an error
 *   occurred an appropriate error code is also set.
 *
 * The function saves the fiber table @em fibers as an attachment to the file
 * @em filename.
 *
 * @see giraffe_fiberlist_load()
 */

cxint
giraffe_fiberlist_save(GiTable *fibers, const cxchar *filename)
{

    const cxchar *fctid = "giraffe_fiberlist_save";

    cxbool created = FALSE;

    cxint code;

    cpl_propertylist *properties = NULL;
    cpl_table *table = NULL;


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

    table = giraffe_table_get(fibers);

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

    properties = giraffe_table_get_properties(fibers);

    if (properties == NULL) {
        properties = cpl_propertylist_new();

        cpl_propertylist_append_string(properties, GIALIAS_EXTNAME,
                                       GIFRAME_FIBER_SETUP);
        created = TRUE;

        giraffe_table_set_properties(fibers, properties);
    }
    else {
        if (cpl_propertylist_has(properties, GIALIAS_EXTNAME)) {
            cpl_propertylist_set_string(properties, GIALIAS_EXTNAME,
                                        GIFRAME_FIBER_SETUP);
        }
        else {
            cpl_propertylist_append_string(properties, GIALIAS_EXTNAME,
                                           GIFRAME_FIBER_SETUP);
        }
    }
    cpl_propertylist_set_comment(properties, GIALIAS_EXTNAME,
                                 "FITS Extension name");

    code = cpl_table_save(table, NULL, properties, filename, CPL_IO_EXTEND);

    if (created == TRUE) {
        cpl_propertylist_delete(properties);
    }

    return code == CPL_ERROR_NONE ? 0 : 1;

}


/**
 * @brief
 *   Attach a fiber table to a frame.
 *
 * @param frame   The frame to which the fiber table is attached.
 * @param fibers  The fiber table to attach.
 *
 * @return The function returns 0 on success or 1 otherwise. If an error
 *   occurred an appropriate error code is also set.
 *
 * The function appends the fiber table @em fibers to the disk file indicated
 * by the filename property of the frame @em frame.
 *
 * @see giraffe_frame_attach_table()
 */

cxint
giraffe_fiberlist_attach(cpl_frame *frame, GiTable *fibers)
{

    const cxchar *fctid = "giraffe_fiberlist_attach";


    cxbool created = FALSE;

    cxint status = 0;

    cpl_propertylist *properties = NULL;

    GiTable *_fibers = NULL;


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

    _fibers = giraffe_table_duplicate(fibers);

    properties = giraffe_table_get_properties(_fibers);

    if (properties == NULL) {
        properties = cpl_propertylist_new();
        giraffe_table_set_properties(_fibers, properties);
        created = TRUE;
    }

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

    status = giraffe_frame_attach_table(frame, _fibers, GIFRAME_FIBER_SETUP,
                                        TRUE);

    if (created == TRUE) {
        cpl_propertylist_delete(properties);
    }

    properties = NULL;

    giraffe_table_delete(_fibers);
    _fibers = NULL;

    return status;

}


/**
 * @brief
 *   Compare two fiber lists.
 *
 * @param fibers     The fiber list to compare with the reference.
 * @param reference  Reference fiber list.
 *
 * @return The function returns 1 if the fiber lists are 'equal', or 0 if
 *   they are not 'equal'. In case an error occurs, the function returns a
 *   negative number.
 *
 * The function verifies that the fiber list @em fibers contains the same
 * set of fibers, or is a sub set of the fibers listed in @em reference.
 * Fibers are identified using their fiber position within the slit, i.e.
 * both tables must have a column named 'FPS'.
 */

cxint giraffe_fiberlist_compare(const GiTable *fibers,
                                const GiTable *reference)
{

    register cxint i;
    cxint equal = 1;

    cpl_table *_fibers = giraffe_table_get(fibers);
    cpl_table *_reference = giraffe_table_get(reference);


    if (_fibers == NULL || _reference == NULL) {
        return -1;
    }

    if (!cpl_table_has_column(_fibers, "FPS") ||
        !cpl_table_has_column(_reference, "FPS")) {
        return -2;
    }


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

        cxbool found = FALSE;

        cxint j;
        cxint fps = cpl_table_get_int(_reference, "FPS", i, NULL);

        for (j = 0; j < cpl_table_get_nrow(_fibers); j++) {
            cxint _fps = cpl_table_get_int(_fibers, "FPS", j, NULL);

            if (fps == _fps) {
                found = TRUE;
                break;
            }
        }

        if (found == FALSE) {
            equal = 0;
            break;
        }

    }

    return equal;

}


/**
 * @brief
 *   Associate a fiberlist with a reference list.
 *
 * @param fibers     Fiber list to associate to a reference list.
 * @param reference  Reference fiber list.
 *
 * @return The function returns 0 on success and a non-zero value in case
 *   an error occurred. In the latter case an appropriate error code is
 *   also set.
 *
 * The function associates all fibers in the source fiber list @em fibers
 * which have a corresponding entry in the reference fiber list
 * @em reference. To associate the source fiber list to the reference list
 * a table column `RINDEX' is added to @em fibers, if it does not already
 * exist. For each fiber having an entry in both tables the appropriate
 * table cell of the column `RINDEX' is filled with the fiber index (column
 * `INDEX') from the reference table.
 */

cxint
giraffe_fiberlist_associate(GiTable *fibers, const GiTable *reference)
{

    const cxchar *fctid = "giraffe_fiberlist_associate";

    register cxint i;

    cxint nf = 0;
    cxint nr = 0;

    cpl_table *_fibers = NULL;
    cpl_table *_reference = NULL;


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

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

    _fibers = giraffe_table_get(fibers);
    _reference = giraffe_table_get(reference);

    if (!cpl_table_has_column(_fibers, "FPS")) {
        cpl_error_set(fctid, CPL_ERROR_ILLEGAL_INPUT);
        return 1;
    }

    if (!cpl_table_has_column(_reference, "FPS")) {
        cpl_error_set(fctid, CPL_ERROR_ILLEGAL_INPUT);
        return 1;
    }


    /*
     * Create new column containing the fiber index of the calibration
     * spectrum in the reference table which is used to process the current
     * spectrum.
     */

    if (!cpl_table_has_column(_fibers, "RINDEX")) {

        cxint size = cpl_table_get_nrow(_fibers);

        cxint status = cpl_table_duplicate_column(_fibers, "RINDEX",
                                                  _fibers, "INDEX");

        if (status != CPL_ERROR_NONE) {
            return 2;
        }

        status = cpl_table_fill_column_window_int(_fibers, "RINDEX", 0,
                                                  size, -1);

        if (status != CPL_ERROR_NONE) {
            return 2;
        }

    }


    /*
     * Write the reference fiber index of all fibers with a corresponding
     * entry in the reference fiber list to the input fiber list and select
     * it. Extract all selected fibers.
     */

    nf = cpl_table_get_nrow(_fibers);
    nr = cpl_table_get_nrow(_reference);

    cpl_table_unselect_all(_fibers);

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

        register cxint j;

        cxint fps = cpl_table_get_int(_fibers, "FPS", i, NULL);


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

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


            if (fps == _fps) {

                cxint ridx = cpl_table_get_int(_reference, "INDEX", j, NULL);

                cpl_table_set_int(_fibers, "RINDEX", i, ridx);
                cpl_table_select_row(_fibers, i);

                break;
            }
        }
    }


    /*
     * From this point on, _fibers is not just a reference anymore, but
     * points to a newly allocated table object, which must be managed
     * properly.
     */

    _fibers = cpl_table_extract_selected(_fibers);


    /*
     * Rewrite index column
     */

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


    giraffe_table_set(fibers, _fibers);

    cpl_table_delete(_fibers);

    return 0;

}


/**
 * @brief
 *  Remove the reference index column from a fiber list.
 *
 * @param fibers  The fiber list to update.
 *
 * @return
 *  The function returns @c 0 on success and a non-zero value otherwise.
 *
 * If the fiber list @em fibers contains a columns @c RINDEX, it is removed
 * from the table.
 */

cxint
giraffe_fiberlist_clear_index(GiTable* fibers)
{

    cpl_table* _fibers = NULL;

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

    _fibers = giraffe_table_get(fibers);

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

    giraffe_error_push();

    if (cpl_table_has_column(_fibers, "RINDEX") == TRUE) {
        cpl_table_erase_column(_fibers, "RINDEX");
    }

    if (cpl_error_get_code() != CPL_ERROR_NONE) {
        return 2;
    }

    giraffe_error_pop();

    return 0;

}
/**
 * @brief
 *  Query a fiber list for the name of the fiber reference index column.
 *
 * @param fibers  The fiber list to query.
 *
 * @return The function returns the name of the reference index column, or
 *   @c NULL if an error occurred.
 *
 * The function queries the fiber list @em fibers for the name of the
 * reference index column to be used when processing fibers. This name
 * refers to the column which contains the index of a given fiber in an
 * associated calibration frame (typically the fiber localization).
 */

const cxchar *
giraffe_fiberlist_query_index(const cpl_table *fibers)
{

    const cxchar *names[] = {"RINDEX", "INDEX", NULL};
    const cxchar **idx = names;


    while (*idx != NULL) {

        if (cpl_table_has_column((cpl_table *)fibers, *idx) != 0) {
            break;
        }

        ++idx;
    }

    return *idx;

}


/**
 * @brief
 *   Get the list of subslit identifiers from a fiber setup.
 *
 * @param fibers  Table containing the fiber setup.
 *
 * @return
 *   The function returns an array with the subslit id numbers, or, in case
 *   an error occurred @c NULL.
 *
 *   The function retrieves all subslit identifiers (SSN numbers) from
 *   the fiber setup @em fibers, for all subslits which contain at least
 *   one fiber.
 */

cpl_array*
giraffe_fiberlist_get_subslits(const cpl_table* fibers)
{

    cxint nfibers = 0;

    cpl_array* subslits = NULL;


    cx_assert(fibers != NULL);

    nfibers = cpl_table_get_nrow(fibers);


    if (nfibers > 0) {

        cxint i = 0;
        cxint nss = 0;
        cxint* ssn = NULL;


        subslits = cpl_array_new(nfibers, CPL_TYPE_INT);
        cpl_array_fill_window(subslits, 0, nfibers, 0);

        ssn = cpl_array_get_data_int(subslits);


        /*
         * Create sorted list of subslit numbers
         */

        for (i = 0; i < nfibers; ++i) {
            ssn[i] = cpl_table_get_int(fibers, "SSN", i, NULL);
        }

        qsort(ssn, nfibers, sizeof(cxint), _giraffe_compare_int);


        /*
         * Remove duplicate subslit numbers from the list
         */

        for (i = 1; i < nfibers; ++i) {
            if (ssn[i] != ssn[nss]) {
                ssn[++nss] = ssn[i];
            }
        }

        ++nss;
        cpl_array_set_size(subslits, nss);

    }

    return subslits;

}


/**
 * @brief
 *   Parses a spectrum selection string.
 *
 * @param selection  The spectrum selection string to parse.
 * @param nspec      The number of selected spectra.
 *
 * @return The function returns an array of spectrum indices of the selected
 *   spectra. The number of the selected spectra is returned in @em nspec.
 *
 * The function parses the spectrum selection string @em selection and
 * creates an array of indices from it. Each entry in the returned array
 * is the number of the selected spectrum, i.e. the position in the slit.
 * Counting of spectra starts at 1.
 *
 * The string to be parsed is a comma separated list of individual spectrum
 * indices (the position of the spectrum within the slit), index ranges
 * with a following, optional step size. Spectra may be excluded explicitly
 * by listing their indices in the same way as described for selected
 * spectra. If spectra should be excluded the list of exclusions must
 * follow the list of selected spectra separated by a semicolon:
 *
 * @code
 *    selection = "1,3,5,10-20,30-60:2;12-15,32";
 * @endcode
 *
 * The created array can be deallocated by a call to @b cx_free().
 */

cxint *
giraffe_parse_spectrum_selection(const cxchar *selection, cxint *nspec)
{

    cxchar **lists = NULL;
    cxchar **ranges = NULL;

    cxint i;
    cxint first = 0;
    cxint nfibers = 0;
    cxint *fibers = NULL;
    cxint *_fibers = NULL;

    cx_slist *fl = NULL;

    cx_slist_iterator pos;


    *nspec = 0;

    lists = cx_strsplit(selection, ";", 2);

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

    if (lists[1] != NULL) {
        gi_warning("Usage of fiber exclusion lists is not supported! "
                   "The given exclusion list is ignored!");
    }

    ranges = cx_strsplit(lists[0], ",", -1);

    if (ranges == NULL) {
        cx_strfreev(lists);
        return NULL;
    }

    i = 0;
    while (ranges[i] != NULL) {

        cxchar **bounds = cx_strsplit(ranges[i], "-", 2);

        cxint j;


        if (bounds == NULL) {
            cx_strfreev(ranges);
            cx_strfreev(lists);

            if (fibers) {
                cx_free(fibers);
            }

            return NULL;
        }
        else {

            cxchar *last;

            cxlong lower = -1;
            cxlong upper = -1;


            lower = strtol(bounds[0], &last, 10);

            if (*last != '\0') {
                cx_strfreev(bounds);
                cx_strfreev(ranges);
                cx_strfreev(lists);

                if (fibers) {
                    cx_free(fibers);
                }

                return NULL;
            }

            if (bounds[1] != NULL) {

                upper = strtol(bounds[1], &last, 10);

                if (*last != '\0') {
                    cx_strfreev(bounds);
                    cx_strfreev(ranges);
                    cx_strfreev(lists);

                    if (fibers) {
                        cx_free(fibers);
                    }

                    return NULL;
                }
            }

            upper = upper > 0 ? upper : lower;

            if (lower <= 0 || upper <= 0 || upper < lower) {
                cx_strfreev(bounds);
                cx_strfreev(ranges);
                cx_strfreev(lists);

                if (fibers) {
                    cx_free(fibers);
                }

                return NULL;
            }

            ++nfibers;

            if (upper > lower) {
                nfibers += upper - lower;
            }

            fibers = cx_realloc(fibers, nfibers * sizeof(cxint));

            for (j = first; j < nfibers; j++) {
                fibers[j] = lower + j - first;
            }

            first = nfibers;

        }

        cx_strfreev(bounds);
        bounds = NULL;

        ++i;

    }

    cx_strfreev(ranges);
    cx_strfreev(lists);

    qsort(fibers, nfibers, sizeof(cxint), _giraffe_compare_int);


    /*
     * Remove duplicates from the fiber list
     */

    fl = cx_slist_new();

    for (i = 0; i < nfibers; i++) {
        cx_slist_push_back(fl, fibers + i);
    }

    cx_slist_unique(fl, _giraffe_compare_int);

    nfibers = cx_slist_size(fl);
    _fibers = cx_malloc(nfibers * sizeof(cxint));

    i = 0;

    pos = cx_slist_begin(fl);
    while (pos != cx_slist_end(fl)) {

        cxint *fn = cx_slist_get(fl, pos);

        cx_assert(fn != NULL);
        _fibers[i] = *fn;

        pos = cx_slist_next(fl, pos);
        ++i;
    }
    cx_slist_delete(fl);
    cx_free(fibers);

    *nspec = nfibers;
    return _fibers;

}


/**
 * @brief
 *   Create a spectrum selection from a reference table.
 *
 * @param filename
 * @param reference
 * @param nspec
 *
 * @return
 */

cxint *
giraffe_create_spectrum_selection(const cxchar *filename,
                                  const GiTable *reference, cxint *nspec)
{

    cpl_table *fibers     = giraffe_fiberlist_create(filename, 0, NULL);
    cpl_table *_reference = giraffe_table_get(reference);

    cxint i        = 0;
    cxint nspectra = 0;
    cxint nfibers  = cpl_table_get_nrow(fibers);
    cxint nactive  = cpl_table_get_nrow(_reference);
    cxint *spectra = NULL;


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

    if (!cpl_table_has_column(fibers, "FPS") ||
        !cpl_table_has_column(fibers, "FPD")) {
        cpl_table_delete(fibers);
        return NULL;
    }

    if (!cpl_table_has_column(_reference, "FPS")) {
        cpl_table_delete(fibers);
        return NULL;
    }

    if (nactive > nfibers) {
        cpl_table_delete(fibers);
        return NULL;
    }

    *nspec  = 0;
    spectra = cx_malloc(nactive * sizeof(cxint));

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

        cxint j   = 0;
        cxint fps = cpl_table_get_int(_reference, "FPS", i, NULL);

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

            cxint _fps = cpl_table_get_int(fibers, "FPS", j, NULL);
            cxint _fpd = cpl_table_get_int(fibers, "FPD", j, NULL);

            if (_fps == fps) {
                spectra[nspectra] = _fpd;
                ++nspectra;
                break;
            }

        }

    }

    cpl_table_delete(fibers);

    if (nspectra < nactive) {
        spectra = cx_realloc(spectra, nspectra * sizeof(cxint));
    }

    qsort(spectra, nspectra, sizeof(cxint), _giraffe_compare_int);
    *nspec = nspectra;

    return spectra;

}
/**@}*/
