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

#include <cxmemory.h>
#include <cxmessages.h>
#include <cxstring.h>

#include <cpl_error.h>
#include <cpl_array.h>

#include "gialias.h"
#include "gitable.h"


/**
 * @defgroup gitable Tables
 *
 * TBD
 */

/**@{*/

struct GiTable {
    cpl_table *data;
    cpl_propertylist *properties;
};

/*
 * Clear the table contents. After calling this function the Giraffe
 * table is empty.
 */

inline static void
_giraffe_table_clear(GiTable *self)
{

    if (self->data) {
        cpl_table_delete(self->data);
        self->data = NULL;
    }

    if (self->properties) {
        cpl_propertylist_delete(self->properties);
        self->properties = NULL;
    }

    return;

}


/**
 * @brief
 *   Creates a new, empty Giraffe table.
 *
 * @return The newly allocated table.
 *
 * The function allocates memory for a new table and initializes it
 * to be empty.
 */

GiTable *
giraffe_table_new(void)
{

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

    self->data = NULL;
    self->properties = NULL;

    return self;

}


/**
 * @brief
 *   Creates a Giraffe table from a table and a property list.
 *
 * @param table       A table from which the new GiTable is created.
 * @param properties  A property list to attach to the created table.
 *
 * @return The newly created and initialized Giraffe table, or @c NULL
 *   in case of errors.
 *
 * The function creates a new Giraffe table from an existing CPL table
 * @em table. The source table is copied. Optionally a property list
 * may be specified, which is attached (copied) to the newly created table
 * if @em properties is not @c NULL.
 */

GiTable *
giraffe_table_create(cpl_table *table, cpl_propertylist *properties)
{

    GiTable *self = NULL;

    if (table) {
        self = giraffe_table_new();

        self->data = cpl_table_duplicate(table);
        if (!self->data) {
            return NULL;
        }

        if (properties) {
            self->properties = cpl_propertylist_duplicate(properties);
            if (!self->properties) {
                giraffe_table_delete(self);
                return NULL;
            }
        }
    }

    return self;

}


/**
 * @brief
 *   Destroys a Giraffe table.
 *
 * @param  self  The table to destroy.
 *
 * @return Nothing.
 *
 * The function deallocates the memory used by @em self.
 */

void
giraffe_table_delete(GiTable *self)
{

    if (self) {
        _giraffe_table_clear(self);
        cx_free(self);
    }

    return;

}

/**
 * @brief
 *   Duplicate a Giraffe table.
 *
 * @return The newly allocated duplicate table.
 *
 * The function duplicates an existing table
 */

GiTable *
giraffe_table_duplicate(const GiTable *src)
{

    GiTable *self = NULL;

    if (src != NULL) {

        cpl_propertylist *properties = giraffe_table_get_properties(src);
        cpl_table *data = giraffe_table_get(src);


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

        if (properties) {
            self->properties = cpl_propertylist_duplicate(properties);
        }

        if (data) {
            self->data = cpl_table_duplicate(data);
        }

    }

    return self;

}





/**
 * @brief
 *   Clears a Giraffe table.
 *
 * @param self  The Giraffe table to be emptied.
 *
 * @return Nothing.
 *
 * The function removes all possibly existing data from the table @em self.
 * After calling this function @em self is empty.
 */

void
giraffe_table_clear(GiTable *self)
{

    if (self) {
        _giraffe_table_clear(self);
    }

    return;

}


/**
 * @brief
 *   Copies matrix elements into a table.
 *
 * @param table   The table to which the matrix elements are copied.
 * @param name    The name of the first target column.
 * @param matrix  The matrix whose elements will be copied.
 *
 * @return The function returns 0 on success, or 1 otherwise.
 *
 * The function copies the matrix elements of the source matrix @em matrix
 * into consecutive columns of the table @em table starting at the column
 * indicated by @em name. The matrix elements are transferred to the
 * table in the following way. The columns of @em matrix will be the
 * columns of the table and their rows will be the elements of the table
 * column. The first row of the table will contain the elements of the
 * first row of the matrix and so on.
 *
 * In order to succeed, the target table's number of rows must match the
 * number of matrix rows, and the number of table columns must not be less
 * than the number of matrix columns (depending on the provided start column).
 *
 * If @em name is @c NULL the first target column is the first column in the
 * table.
 */

cxint
giraffe_table_copy_matrix(GiTable *table, const cxchar *name,
                          cpl_matrix *matrix)
{

    const cxchar *fctid = "giraffe_table_copy_matrix";

    cxint nrow = 0;
    cxint ncol = 0;
    cxint count = 0;

    cpl_array *labels = NULL;
    cpl_table *_table = NULL;


    cx_assert(table != NULL);

    if (!matrix) {
        return 1;
    }

    nrow = cpl_matrix_get_nrow(matrix);
    ncol = cpl_matrix_get_ncol(matrix);
    cx_assert(nrow > 0 && ncol > 0);

    _table = giraffe_table_get(table);

    labels = cpl_table_get_column_names(_table);
    cx_assert(cpl_array_get_size(labels) > 0);


    if (name != NULL) {

        if (cpl_table_has_column(_table, name) == 0) {

            cpl_array_delete(labels);
            cpl_error_set(fctid, CPL_ERROR_DATA_NOT_FOUND);

            return 1;

        }
        else {

            /*
             * Move to the first target column. No check whether count goes
             * out of range is needed here since it is already known that
             * a column with name 'name' exists.
             */

            while (strcmp(cpl_array_get_string(labels, count), name) != 0) {
                ++count;
            }

        }

    }


    if (cpl_table_get_nrow(_table) != nrow ||
        cpl_table_get_ncol(_table) - count < ncol) {

        cpl_array_delete(labels);
        cpl_error_set(fctid, CPL_ERROR_INCOMPATIBLE_INPUT);

        return 1;

    }
    else {

        cxint i;

        cxdouble *elements = cpl_matrix_get_data(matrix);


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

            const cxchar *label = cpl_array_get_string(labels, count + i);

            cpl_type type = cpl_table_get_column_type(_table, label);


            switch (type) {
                case CPL_TYPE_INT:
                {
                    register cxint j;

                    for (j = 0; j < nrow; ++j) {
                        cpl_table_set_int(_table, label, j,
                                          (cxint) elements[j * ncol + i]);
                    }
                    break;
                }

                case CPL_TYPE_FLOAT:
                {
                    register cxint j;

                    for (j = 0; j < nrow; ++j) {
                        cpl_table_set_float(_table, label, j,
                                            (cxfloat) elements[j * ncol + i]);
                    }
                    break;
                }

                case CPL_TYPE_DOUBLE:
                {
                    register cxint j;

                    for (j = 0; j < nrow; ++j) {
                        cpl_table_set_double(_table, label, j,
                                             elements[j * ncol + i]);
                    }
                    break;
                }

                case CPL_TYPE_STRING:
                default:
                {
                    cpl_array_delete(labels);
                    cpl_error_set(fctid, CPL_ERROR_INVALID_TYPE);

                    return 1;
                    break;
                }
            }

        }

    }

    cpl_array_delete(labels);

    return 0;

}


/**
 * @brief
 *   Check whether a Giraffe table is empty.
 *
 * @param self  The table to check.
 *
 * @return The function returns 1 if the table is empty and 0 otherwise.
 *
 * The function checks whether @em self contains any table data or properties.
 * A table is empty if it contains no data and no properties.
 */

cxint
giraffe_table_is_empty(GiTable *self)
{

    if (self->data) {
        return 0;
    }

    return self->properties == NULL ? 1 : 0;

}


/**
 * @brief
 *   Get the table data from a Giraffe table.
 *
 * @param self  A Giraffe table.
 *
 * @return  Pointer to the table data @em self contains.
 *
 * The function returns a pointer to @em self's table data. The pointer
 * might be a @c NULL pointer in case of empty tables for instance.
 */

cpl_table *
giraffe_table_get(const GiTable *self)
{

    cx_assert(self != NULL);

    return self->data;

}


/**
 * @brief
 *   Sets the table data.
 *
 * @param self   The table to store the data.
 * @param table  The source table the data is taken from.
 *
 * @return The function returns 0 on success, or 1 in case of errors.
 *
 * The function copies the table data of @em table into @em self.
 */

cxint
giraffe_table_set(GiTable *self, cpl_table *table)
{

    cpl_table *tmp = NULL;

    tmp = giraffe_table_get(self);

    cx_assert(table != NULL);

    if (tmp != NULL) {
        cpl_table_delete(tmp);
    }

    self->data = cpl_table_duplicate(table);

    return 0;

}

/**
 * @brief
 *   Gets the table properties.
 *
 * @param self  A Giraffe table.
 *
 * @return  Pointer to the properties attached to @em self.
 *
 * The function returns a pointer to properties @em self contains. The
 * returned pointer might be a @c NULL pointer if no properties are
 * attached to @em self.
 */

cpl_propertylist *
giraffe_table_get_properties(const GiTable *self)
{

    cx_assert(self != NULL);

    return self->properties;

}

/**
 * @brief
 *   Attaches a property list to an table.
 *
 * @param self        Table the properties will be attached to.
 * @param properties  Property list to attach.
 *
 * @return The function returns 0 on success, or 1 otherwise.
 *
 * The function stores a copy of the property list @em properties
 * in the table container @em self.
 *
 * If there is already a property list stored in the container @em self, it is
 * replaced with the reference @em properties. If the properties cannot be
 * copied, the function returns an error.
 */

cxint
giraffe_table_set_properties(GiTable *self, cpl_propertylist *properties)
 {

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

    if (self->properties) {
        cpl_propertylist_delete(self->properties);
        self->properties = NULL;
    }

    self->properties = cpl_propertylist_duplicate(properties);

    return self->properties ? 0 : 1;

}

/**
 * @brief
 *   Reads a data set from a file into a Giraffe table.
 *
 * @param self      The Giraffe table to fill.
 * @param filename  Name of the file.
 * @param position  Position of the data set within the file from which the
 *                  table is created.
 * @param id        Data set identifier
 *
 * @return The function returns 0 on success, or a non-zero number in
 *   case of errors and sets the appropriate error code. If @em id is not
 *   @c NULL and the validation of the data set name fails the error
 *   @c CPL_ERROR_BAD_FILE_FORMAT. If the loading of either the table data,
 *   or the table properties fails the error codes are the ones set by
 *   @b cpl_table_load() and @b cpl_propertylist_load() respectively.
 *
 * The function reads the data set number @em position from the
 * file @em filename, allocates a new Giraffe table and fills it with the
 * data set. Data sets are counted from zero.
 *
 * If @em id, the data set identifier, is not @c NULL the given string is
 * compared to the data set name. If this does not match @em id the function
 * returns an error. If @em id is set to @c NULL the check on the data set
 * name is skipped.
 */

cxint
giraffe_table_load(GiTable *self, const cxchar *filename, cxint position,
                   const cxchar *id)
{

    const cxchar *fctid = "giraffe_table_load";


    if (!self || !filename) {
        return 1;
    }

    self->data = cpl_table_load((cxchar *)filename, position, 0);
    if (!self->data) {
        if (cpl_error_get_code() == CPL_ERROR_NULL_INPUT) {
            cpl_error_set(fctid, CPL_ERROR_DATA_NOT_FOUND);
            return 2;
        }

        return 1;
    }

    self->properties = cpl_propertylist_load(filename, position);

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

    if (id) {
        cxint status = 1;

        if (self->properties &&
            cpl_propertylist_has(self->properties, GIALIAS_EXTNAME)) {

            const cxchar *magic;

            magic = cpl_propertylist_get_string(self->properties,
                                                GIALIAS_EXTNAME);

            if (strcmp(id, magic) == 0) {
                status = 0;
            }

        }

        if (status != 0) {
            _giraffe_table_clear(self);
            cpl_error_set(fctid, CPL_ERROR_BAD_FILE_FORMAT);
            return 1;
        }

    }

    return 0;

}


/**
 * @brief
 *   Write a Giraffe table to a file.
 *
 * @param  self      Giraffe table to write.
 * @param  filename  File to which @em self is written.
 *
 * @return The function returns 0 on success and 1 otherwise.
 *
 * The Giraffe table @em self is written to the file @em filename. Currently
 * a Giraffe table can only be written as a FITS table with the table data
 * in the primary data unit. The image properties are written/converted to
 * the primary FITS header.
 */

cxint
giraffe_table_save(GiTable *self, const cxchar *filename)
{

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

    if (self) {

        cxint code;

        cpl_table *table = giraffe_table_get(self);
        cpl_propertylist *plist0 = giraffe_table_get_properties(self);
        cpl_propertylist *plist1 = NULL;


        if (cpl_propertylist_is_empty(plist0)) {
            plist0 = NULL;
        }

        plist0 = cpl_propertylist_duplicate(plist0);


        /*
         * Remove keywords which may not appear in table headers.
         */

        cpl_propertylist_erase(plist0, "BSCALE");
        cpl_propertylist_erase(plist0, "BZERO");
        cpl_propertylist_erase(plist0, "BUNIT");

        cpl_propertylist_erase(plist0, "DATAMIN");
        cpl_propertylist_erase(plist0, "DATAMAX");


        /* FIXME: Workaround for CPL deficiency. World coordinate
         *        keywords are not removed from table headers.
         */

        cpl_propertylist_erase_regexp(plist0, "^CRPIX[0-9]$", 0);
        cpl_propertylist_erase_regexp(plist0, "^CRVAL[0-9]$", 0);
        cpl_propertylist_erase_regexp(plist0, "^CDELT[0-9]$", 0);
        cpl_propertylist_erase_regexp(plist0, "^CTYPE[0-9]$", 0);


        /*
         * Split properties into separate property lists for the primary
         * and the secondary header
         */

        plist1 = cpl_propertylist_duplicate(plist0);


        /*
         * Remove header specific keywords from the respective
         * property list.
         */

        cpl_propertylist_erase(plist0, "EXTNAME");

        cpl_propertylist_erase(plist1, "DATAMD5");
        cpl_propertylist_erase(plist1, "INHERIT");
        cpl_propertylist_erase(plist1, "PIPEFILE");
        cpl_propertylist_erase(plist1, GIALIAS_ANCESTOR);


        /*
         * Save the table to disk
         */

        code = cpl_table_save(table, plist0, plist1, filename, CPL_IO_CREATE);

        if (code != CPL_ERROR_NONE) {
            cpl_propertylist_delete(plist0);
            plist0 = NULL;

            cpl_propertylist_delete(plist1);
            plist1 = NULL;

            return 1;
        }

        cpl_propertylist_delete(plist0);
        plist0 = NULL;

        cpl_propertylist_delete(plist1);
        plist1 = NULL;

    }

    return 0;

}


/**
 * @brief
 *   Attach a Giraffe table to a file.
 *
 * @param  self      Giraffe table to attach.
 * @param  filename  File to which @em self is attached.
 * @param  position  Position of the data set within the file.
 * @param  id        Data set identifier.
 *
 * @return The function returns 0 on success and 1 otherwise.
 *
 * The Giraffe table @em self is attached to the file @em filename as data
 * set number @em position. If the data set identifier @em id is not @c NULL
 * this name is added to the table properties, overwriting an already set
 * identifier. The data set position must be greater or equal to 1.
 */

cxint
giraffe_table_attach(GiTable *self, const cxchar *filename, cxint position,
                     const cxchar *id)
{

    cxint code;

    cpl_table *table = NULL;
    cpl_propertylist *plist = NULL;


    cx_assert(self != NULL);

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

    if (position < 1) {
        return 1;
    }

    table = giraffe_table_get(self);
    plist = giraffe_table_get_properties(self);

    plist = cpl_propertylist_duplicate(plist);


    /*
     * Remove keywords which may not appear in table headers.
     */

    cpl_propertylist_erase(plist, "BSCALE");
    cpl_propertylist_erase(plist, "BZERO");
    cpl_propertylist_erase(plist, "BUNIT");

    cpl_propertylist_erase(plist, "DATAMIN");
    cpl_propertylist_erase(plist, "DATAMAX");
    cpl_propertylist_erase(plist, "DATAMD5");
    cpl_propertylist_erase(plist, "INHERIT");
    cpl_propertylist_erase(plist, "PIPEFILE");
    cpl_propertylist_erase(plist, GIALIAS_ANCESTOR);

    /* FIXME: Workaround for CPL deficiency. World coordinate
     *        keywords are not removed from table headers.
     */

    cpl_propertylist_erase_regexp(plist, "^CRPIX[0-9]$", 0);
    cpl_propertylist_erase_regexp(plist, "^CRVAL[0-9]$", 0);
    cpl_propertylist_erase_regexp(plist, "^CDELT[0-9]$", 0);
    cpl_propertylist_erase_regexp(plist, "^CTYPE[0-9]$", 0);


    if (id != NULL) {
        cpl_propertylist_update_string(plist, GIALIAS_EXTNAME, id);
        cpl_propertylist_set_comment(plist, GIALIAS_EXTNAME,
                                     "FITS Extension name");
    }
    else {
        if (cpl_propertylist_is_empty(plist)) {
            plist = NULL;
        }
    }

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

    if (code != CPL_ERROR_NONE) {
        cpl_propertylist_delete(plist);
        plist = NULL;

        return 1;
    }

    cpl_propertylist_delete(plist);
    plist = NULL;

    return 0;

}


/**
 * @brief
 *   Add additional frame information to a table.
 *
 * TBD
 */

cxint
giraffe_table_add_info(GiTable *table, const GiRecipeInfo *info ,
                       const cpl_frameset *set)
{

    cxint status = 0;

    cpl_propertylist *properties = NULL;


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

    properties = giraffe_table_get_properties(table);

    if (properties == NULL) {
        return -2;
    }

    if (info != NULL) {
        status = giraffe_add_recipe_info(properties, info);

        if (status != 0) {
            return -3;
        }

        if (set != NULL) {
            status = giraffe_add_frameset_info(properties, set,
                                               info->sequence);


            if (status != 0) {
                return -4;
            }
        }
    }

    return 0;

}
/**@}*/
