/* $Id$
 *
 * This file is part of the GIRAFFE Pipeline
 * Copyright (C) 2002-2006 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
 */

/*
 * $Author$
 * $Date$
 * $Revision$
 * $Name$
 */

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

#ifdef HAVE_SYS_TYPES_H
#  include <sys/types.h>
#endif
#include <time.h>
#include <string.h>
#include <regex.h>

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

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

#include "gialias.h"
#include "gimessages.h"
#include "giutils.h"


/**
 * @defgroup giutils Miscellaneous Utilities
 *
 * TBD
 */

/**@{*/

/*
 * Giraffe version and license
 */

static const cxchar *_giraffe_license =
 " This file is part of the GIRAFFE Instrument Pipeline\n"
 " Copyright (C) 2002-2006 European Southern Observatory\n"
 "\n"
 " This program is free software; you can redistribute it and/or modify\n"
 " it under the terms of the GNU General Public License as published by\n"
 " the Free Software Foundation; either version 2 of the License, or\n"
 " (at your option) any later version.\n"
 "\n"
 " This program is distributed in the hope that it will be useful,\n"
 " but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
 " MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n"
 " GNU General Public License for more details.\n"
 "\n"
 " You should have received a copy of the GNU General Public License\n"
 " along with this program; if not, write to the Free Software\n"
 " Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301"
 "  USA";


inline static cxint
_giraffe_plist_append(cpl_propertylist *self, cpl_property *p)
{

    const cxchar *name = cpl_property_get_name(p);
    const cxchar *comment = cpl_property_get_comment(p);


    switch (cpl_property_get_type(p)) {
    case CPL_TYPE_BOOL:
    {
        cxbool value = cpl_property_get_bool(p);

        cpl_propertylist_append_bool(self, name, value);
        break;
    }

    case CPL_TYPE_CHAR:
    {
        cxchar value = cpl_property_get_char(p);

        cpl_propertylist_append_char(self, name, value);
        break;
    }

    case CPL_TYPE_INT:
    {
        cxint value = cpl_property_get_int(p);

        cpl_propertylist_append_int(self, name, value);
        break;
    }

    case CPL_TYPE_LONG:
    {
        cxlong value = cpl_property_get_long(p);

        cpl_propertylist_append_long(self, name, value);
        break;
    }

    case CPL_TYPE_FLOAT:
    {
        cxfloat value = cpl_property_get_float(p);

        cpl_propertylist_append_float(self, name, value);
        break;
    }

    case CPL_TYPE_DOUBLE:
    {
        cxdouble value = cpl_property_get_double(p);

        cpl_propertylist_append_double(self, name, value);
        break;
    }

    case CPL_TYPE_STRING:
    {
        const cxchar *value = cpl_property_get_string(p);

        cpl_propertylist_append_string(self, name, value);
        break;
    }

    default:

        /*
         * We should never reach this point! Since the property
         * was a valid property it has a valid type. But this
         * protects against addition of new types.
         */

        return 1;
        break;
    }

    if (comment != NULL) {
        cpl_propertylist_set_comment(self, name, comment);
    }

    return 0;

}


inline static cxint
_giraffe_add_frame_info(cpl_propertylist *plist, const cxchar *name,
                        const cxchar *tag, cxint sequence, cxint frame_index,
                        cxint mode)
{

    const cxchar *id = NULL;
    const cxchar *group = NULL;

    cxint status = 0;

    cx_string *key = NULL;
    cx_string *comment = NULL;


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

    switch (mode) {
    case 0:
        id = "RAW";
        group = "raw";
        break;

    case 1:
        id = "CAL";
        group = "calibration";
        break;

    default:
        return 2;
        break;
    }

    key = cx_string_new();
    comment = cx_string_new();


    /*
     * Frame name
     */

    cx_string_sprintf(key, "%s%-d %s%-d %s", "ESO PRO REC", sequence, id,
                      frame_index, "NAME");
    cx_string_sprintf(comment, "%s %s %s", "File name of", group, "frame");

    status = cpl_propertylist_update_string(plist, cx_string_get(key), name);

    if (status != CPL_ERROR_NONE) {
        cx_string_delete(key);
        cx_string_delete(comment);

        return 3;
    }

    status = cpl_propertylist_set_comment(plist, cx_string_get(key),
                                   cx_string_get(comment));

    if (status != 0) {
        cx_string_delete(key);
        cx_string_delete(comment);

        return 3;
    }


    /*
     * Frame category
     */

    cx_string_sprintf(key, "%s%-d %s%-d %s", "ESO PRO REC", sequence, id,
                      frame_index, "CATG");
    cx_string_sprintf(comment, "%s %s %s", "Frame category of", group,
                      "frame");

    status = cpl_propertylist_update_string(plist, cx_string_get(key), tag);

    if (status != CPL_ERROR_NONE) {
        cx_string_delete(key);
        cx_string_delete(comment);

        return 4;
    }

    status = cpl_propertylist_set_comment(plist, cx_string_get(key),
                                   cx_string_get(comment));

    if (status != 0) {
        cx_string_delete(key);
        cx_string_delete(comment);

        return 4;
    }

    cx_string_delete(key);
    cx_string_delete(comment);

    return 0;

}


/**
 * @brief
 *   Get the pipeline copyright and license.
 *
 * @return The copyright and license string.
 *
 * The function returns a pointer to the statically allocated license string.
 * This string should not be modified using the returned pointer.
 */

const cxchar *
giraffe_get_license(void)
{

    return _giraffe_license;

}


/**
 * @brief
 *   Determines the instrument mode from a property list.
 *
 * @param properties  The property list to query.
 *
 * @return The function returns the used instrument mode, or @c GIMODE_NONE
 *   if the mode cannot be determined or an error occurs.
 *
 * The function determines the instrument mode by querying the properties
 * @em properties.
 */

GiInstrumentMode
giraffe_get_mode(cpl_propertylist *properties)
{

    const cxchar *fctid = "giraffe_get_mode";
    const cxchar *mode;

    cx_string *s = NULL;

    GiInstrumentMode insmode;


    if (!properties) {
        cpl_error_set(fctid, CPL_ERROR_NULL_INPUT);
        return GIMODE_NONE;
    }


    if (!cpl_propertylist_has(properties, GIALIAS_INSMODE)) {
        gi_warning("%s: Property (%s) not found\n", fctid, GIALIAS_INSMODE);

        if (!cpl_propertylist_has(properties, GIALIAS_SLITNAME)) {
            cx_warning("%s: Property (%s) not found\n", fctid,
                       GIALIAS_SLITNAME);
            return GIMODE_NONE;
        }
        else {
            mode = cpl_propertylist_get_string(properties, GIALIAS_SLITNAME);
        }
    }
    else {
        mode = cpl_propertylist_get_string(properties, GIALIAS_SLITNAME);
    }

    if (!mode || strlen(mode) == 0) {
        cpl_error_set(fctid, CPL_ERROR_ILLEGAL_INPUT);
        return GIMODE_NONE;
    }


    s = cx_string_create(mode);
    cx_string_lower(s);

    if (strncmp(cx_string_get(s), "med", 3) == 0) {
        insmode = GIMODE_MEDUSA;
    }
    else if (strncmp(cx_string_get(s), "ifu", 3) == 0) {
        insmode = GIMODE_IFU;
    }
    else if (strncmp(cx_string_get(s), "arg", 3) == 0) {
        insmode = GIMODE_ARGUS;
    }
    else {
        cpl_error_set(fctid, CPL_ERROR_UNSUPPORTED_MODE);
        insmode = GIMODE_NONE;
    }

    cx_string_delete(s);

    return insmode;

}

/**
 * @brief
 *    Gets the name of a file without any leading directory components.
 *
 * @param path  The name of the file path.
 *
 * @return The name of the file with any leading directory components
 *   removed.
 *
 * Gets the name of the file without any leading directory components. The
 * function allocates a buffer containing the stripped file name. The
 * returned string should be deallocated using cx_free() when it is no
 * longer needed.
 */

cxchar *
giraffe_path_get_basename(const cxchar *path)
{

    register cxssize base;
    register cxssize last_nonslash;

    cxsize len;

    cxchar *result;


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

    if (path[0] == '\0') {
        return cx_strdup(".");
    }

    last_nonslash = strlen(path) - 1;

    while (last_nonslash >= 0 && path[last_nonslash] == '/') {
        --last_nonslash;
    }


    /* String only containing slashes */

    if (last_nonslash == -1) {
        return cx_strdup("/");
    }

    base = last_nonslash;

    while (base >=0 && path[base] != '/') {
        --base;
    }

    len = last_nonslash - base;

    result = cx_malloc(len + 1);
    memcpy(result, path + base + 1, len);
    result[len] = '\0';

    return result;

}


/**
 * @brief
 *   Get the current date and time in ISO 8601 format.
 *
 * @return The string containing the current date and time in the
 *   ISO 8601 format. If an error occurs the function returns @c NULL.
 *
 * The function formats the current date and the local system time according
 * to the ISO 8601 format and puts both together in a string. The returned
 * string must be deallocated using cx_free().
 */

cxchar *
giraffe_localtime_iso8601(void)
{

    struct tm *ts;

    time_t seconds = time(NULL);

    cxchar *sdate = NULL;

    cxulong milliseconds = 0;

    cx_string *self = cx_string_new();


    cx_assert(self != NULL);

    ts = localtime(&seconds);

    cx_string_sprintf(self, "%4d-%02d-%02dT%02d:%02d:%02d.%03ld",
                      ts->tm_year + 1900,
                      ts->tm_mon + 1,
                      ts->tm_mday,
                      ts->tm_hour,
                      ts->tm_min,
                      ts->tm_sec,
                      milliseconds);

    sdate = cx_strdup(cx_string_get(self));
    cx_string_delete(self);

    return sdate;

}


/**
 * @brief
 *   Add recipe specific information to a property list.
 *
 * TBD
 */

cxint
giraffe_add_recipe_info(cpl_propertylist *plist, const GiRecipeInfo *info)
{

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

    if (info != NULL) {

        cxint status = 0;

        cx_string *name = cx_string_new();
        cx_string *value = cx_string_new();


        cx_string_sprintf(name, "%s%-d %s", "ESO PRO REC", info->sequence,
                          "PIPE ID");
        cx_string_sprintf(value, "%s/%s", PACKAGE, VERSION);

        status = cpl_propertylist_update_string(plist, cx_string_get(name),
                                                cx_string_get(value));


        if (status != CPL_ERROR_NONE) {
            cx_string_delete(name);
            cx_string_delete(value);

            return 1;
        }

        status = cpl_propertylist_set_comment(plist, cx_string_get(name),
                                       "Pipeline (unique) identifier");

        if (status != 0) {
            cx_string_delete(name);
            cx_string_delete(value);

            return 1;
        }

        if (info->start != NULL) {
            cx_string_sprintf(name, "%s%-d %s", "ESO PRO REC",
                              info->sequence, "START");
            status = cpl_propertylist_update_string(plist,
                                                    cx_string_get(name),
                                                    info->start);

            if (status != CPL_ERROR_NONE) {
                cx_string_delete(name);
                cx_string_delete(value);

                return 1;
            }

            status = cpl_propertylist_set_comment(plist, cx_string_get(name),
                                           "Date when recipe execution "
                                           "started.");

            if (status != 0) {
                cx_string_delete(name);
                cx_string_delete(value);

                return 1;
            }
        }

        cx_string_delete(name);
        cx_string_delete(value);

    }

    return 0;

}


/**
 * @brief
 *   Add frameset specific information to a property list.
 *
 * @param plist     The propertylist to update.
 * @param set       A reference frameset
 * @param sequence  Recipe sequence number
 *
 * @return
 *   The function returns 0 on success, or a non-zero number in case an
 *   error occurred.
 *
 * The function adds DFS specific information to the property list
 * @em plist, used for product frames. This information, name, tag, and
 * MD5SUM of the files used during processing are taken from the contents
 * of the reference frameset @em set.
 *
 * The recipe sequence number @em sequence is the index of a recipe within
 * a recipe chain. For single recipe calls this is always @c 1.
 */

cxint
giraffe_add_frameset_info(cpl_propertylist *plist, const cpl_frameset *set,
                          cxint sequence)
{

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

    if (set != NULL) {

        cxsize nraw = 0;
        cxsize ncalib = 0;

        cx_string *key = cx_string_new();

        const cpl_frame *frame = cpl_frameset_get_first_const(set);

        while (frame != NULL) {

            cxint status = 0;

            cpl_frame_group group = cpl_frame_get_group(frame);

            const cxchar *name = cpl_frame_get_filename(frame);
            const cxchar *tag = cpl_frame_get_tag(frame);
            const cxchar *base = giraffe_path_get_basename(name);


            cx_assert(base != NULL);

            switch (group) {
            case CPL_FRAME_GROUP_RAW:
            {
                ++nraw;
                status = _giraffe_add_frame_info(plist, base, tag, sequence,
                                                 nraw, 0);

                if (status != 0) {
                    if (base != NULL) {
                        cx_free((cxchar *)base);
                    }

                    cx_string_delete(key);
                    return -2;
                }

                break;
            }

            case CPL_FRAME_GROUP_CALIB:
            {

                cpl_propertylist *_plist = NULL;


                ++ncalib;
                status = _giraffe_add_frame_info(plist, base, tag, sequence,
                                                 ncalib, 1);

                if (status != 0) {
                    if (base != NULL) {
                        cx_free((cxchar *)base);
                    }

                    cx_string_delete(key);
                    return -3;
                }

                _plist = cpl_propertylist_load(name, 0);

                if (_plist == NULL) {
                    if (base != NULL) {
                        cx_free((cxchar *)base);
                    }

                    cx_string_delete(key);
                    return -3;
                }


                if (cpl_propertylist_has(_plist, GIALIAS_DATAMD5)) {

                    const cxchar *s =
                        cpl_propertylist_get_string(_plist, GIALIAS_DATAMD5);

                    if (strcmp(s, "Not computed") != 0) {

                        cx_string* md5 = cx_string_new();

                        cx_string_sprintf(md5, "%s%d %s%"
                                          CX_PRINTF_FORMAT_SIZE_TYPE "%s",
                                          "ESO PRO REC", sequence, "CAL",
                                          ncalib, "DATAMD5");

                        status =
                            cpl_propertylist_update_string(plist,
                                                           cx_string_get(md5),
                                                           s);

                        if (status != CPL_ERROR_NONE) {
                            cx_string_delete(md5);
                            cpl_propertylist_delete(_plist);

                            if (base != NULL) {
                                cx_free((cxchar *)base);
                            }

                            cx_string_delete(key);
                            return -3;
                        }

                        cx_string_delete(md5);
                    }

                }

                cpl_propertylist_delete(_plist);
                break;
            }

            default:
                break;
            }


            if (base != NULL) {
                cx_free((cxchar *)base);
            }

            frame = cpl_frameset_get_next_const(set);

        }

        cx_string_delete(key);

    }

    return 0;

}


/**
 * @brief
 *   Update a property list.
 *
 * @param self        The property list to update.
 * @param properties  The source property list.
 * @param regexp     A property name pattern.
 *
 * @return The function returns 0 on success and a non-zero value in case an
 *   error occurred. In the latter case an error code is also set.
 *
 * The function updates the target property list with properties from the
 * source list @em properties, which are not present in @em self. If a
 * pattern string is given only properties with names matching the given
 * pattern @em regexp are taken into account when @em self is updated. If
 * a pattern is given, it must be a valid regular expression. If the
 * pattern string is either @c NULL or the empty string, the whole
 * source list is considered during the update operation.
 */

cxint
giraffe_propertylist_update(cpl_propertylist *self,
                            cpl_propertylist *properties,
                            const cxchar *regexp)
{

    const cxchar *fctid = "giraffe_propertylist_update";

    cxlong i;
    cxlong sz = 0;


    cx_assert(self != NULL);

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

    sz = cpl_propertylist_get_size(properties);

    if (regexp == NULL || regexp[0] == '\0') {

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

            cpl_property *p = cpl_propertylist_get(properties, i);
            const cxchar *name = cpl_property_get_name(p);


            if (!cpl_propertylist_has(self, name)) {

                cxint status = _giraffe_plist_append(self, p);

                if (status) {
                    cpl_error_set(fctid, CPL_ERROR_INVALID_TYPE);
                    return 1;
                }
            }
        }
    }
    else {

        cxint status = 0;

        regex_t re;


        status = regcomp(&re, regexp, REG_EXTENDED | REG_NOSUB);

        if (status) {
            cpl_error_set(fctid, CPL_ERROR_ILLEGAL_INPUT);
            return 1;
        }

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

            cpl_property *p = cpl_propertylist_get(properties, i);
            const cxchar *name = cpl_property_get_name(p);


            if (regexec(&re, name, (size_t)0, NULL, 0) == REG_NOMATCH) {
                continue;
            }

            if (!cpl_propertylist_has(self, name)) {

                status = _giraffe_plist_append(self, p);

                if (status) {
                    cpl_error_set(fctid, CPL_ERROR_INVALID_TYPE);
                    return 1;
                }
            }
        }

        regfree(&re);

    }

    return 0;

}


/**
 * @brief
 *   Copy a property from one list to another.
 *
 * TBD
 */

cxint
giraffe_propertylist_copy(cpl_propertylist *self,
                          const cxchar *name,
                          const cpl_propertylist *other,
                          const cxchar *othername)
{

    const cxchar *fctid = "giraffe_propertylist_copy";


    const cxchar *s;
    const cxchar *comment;

    cpl_type type;



    cx_assert(self != NULL);

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

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

    if (!cpl_propertylist_has(other, othername)) {
        return 1;
    }

    type = cpl_propertylist_get_type(other, othername);


    /*
     * Determine the name the new property should have in self.
     */

    s = name == NULL ? othername : name;


    /*
     * Add the property to the destination list.
     */

    switch (type) {
        case CPL_TYPE_CHAR:
        {
            cxchar value = cpl_propertylist_get_char(other, othername);

            if (cpl_propertylist_has(self, s)) {
                cpl_propertylist_set_char(self, s, value);
            }
            else {
                cpl_propertylist_append_char(self, s, value);
            }
        }
        break;

        case CPL_TYPE_BOOL:
        {
            cxbool value = cpl_propertylist_get_bool(other, othername);

            if (cpl_propertylist_has(self, s)) {
                cpl_propertylist_set_bool(self, s, value);
            }
            else {
                cpl_propertylist_append_bool(self, s, value);
            }
        }
        break;

        case CPL_TYPE_INT:
        {
            cxint value = cpl_propertylist_get_int(other, othername);

            if (cpl_propertylist_has(self, s)) {
                cpl_propertylist_set_int(self, s, value);
            }
            else {
                cpl_propertylist_append_int(self, s, value);
            }
        }
        break;

        case CPL_TYPE_LONG:
        {
            cxlong value = cpl_propertylist_get_long(other, othername);

            if (cpl_propertylist_has(self, s)) {
                cpl_propertylist_set_long(self, s, value);
            }
            else {
                cpl_propertylist_append_long(self, s, value);
            }
        }
        break;

        case CPL_TYPE_FLOAT:
        {
            cxfloat value = cpl_propertylist_get_float(other, othername);

            if (cpl_propertylist_has(self, s)) {
                cpl_propertylist_set_float(self, s, value);
            }
            else {
                cpl_propertylist_append_float(self, s, value);
            }
        }
        break;

        case CPL_TYPE_DOUBLE:
        {
            cxdouble value = cpl_propertylist_get_double(other, othername);

            if (cpl_propertylist_has(self, s)) {
                cpl_propertylist_set_double(self, s, value);
            }
            else {
                cpl_propertylist_append_double(self, s, value);
            }
        }
        break;

        case CPL_TYPE_STRING:
        {
            const cxchar *value = cpl_propertylist_get_string(other,
                                                              othername);

            if (cpl_propertylist_has(self, s)) {
                cpl_propertylist_set_string(self, s, value);
            }
            else {
                cpl_propertylist_append_string(self, s, value);
            }
        }
        break;

        default:
            cpl_error_set(fctid, CPL_ERROR_INVALID_TYPE);
            return 2;
            break;
    }

    comment = cpl_propertylist_get_comment(other, othername);

    if (comment != NULL) {
        cpl_propertylist_set_comment(self, s, comment);
    }

    return 0;

}


cxint
giraffe_propertylist_update_wcs(cpl_propertylist* properties, cxsize naxis,
                                const cxdouble* crpix, const cxdouble* crval,
                                const cxchar** ctype, const cxchar** cunit,
                                const cpl_matrix* cd)
{

    if (properties == NULL) {
        return 0;
    }

    cpl_propertylist_erase_regexp(properties, "^CRPIX[0-9]", 0);
    cpl_propertylist_erase_regexp(properties, "^CRVAL[0-9]", 0);
    cpl_propertylist_erase_regexp(properties, "^CDELT[0-9]", 0);
    cpl_propertylist_erase_regexp(properties, "^CTYPE[0-9]", 0);
    cpl_propertylist_erase_regexp(properties, "^CUNIT[0-9]", 0);
    cpl_propertylist_erase_regexp(properties, "^CROTA[0-9]", 0);
    cpl_propertylist_erase_regexp(properties, "^CD[0-9]*_[0-9]", 0);
    cpl_propertylist_erase_regexp(properties, "^PC[0-9]*_[0-9]", 0);


    if (naxis > 0) {


        register cxsize i = 0;

        cx_string* keyword = cx_string_new();
        cx_string* comment = cx_string_new();


        cx_assert(cpl_matrix_get_nrow(cd) == cpl_matrix_get_ncol(cd));

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

            cx_string_sprintf(keyword, "CTYPE%-" CX_PRINTF_FORMAT_SIZE_TYPE,
                              i + 1);
            cx_string_sprintf(comment, "Coordinate system of axis %"
                              CX_PRINTF_FORMAT_SIZE_TYPE, i + 1);
            cpl_propertylist_append_string(properties,
                                           cx_string_get(keyword), ctype[i]);
            cpl_propertylist_set_comment(properties, cx_string_get(keyword),
                                         cx_string_get(comment));

        }

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

            cx_string_sprintf(keyword, "CRPIX%-" CX_PRINTF_FORMAT_SIZE_TYPE,
                              i + 1);
            cx_string_sprintf(comment, "Reference pixel of axis %"
                              CX_PRINTF_FORMAT_SIZE_TYPE, i + 1);
            cpl_propertylist_append_double(properties,
                                           cx_string_get(keyword), crpix[i]);
            cpl_propertylist_set_comment(properties, cx_string_get(keyword),
                                         cx_string_get(comment));

        }

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

            cx_string_sprintf(keyword, "CRVAL%-" CX_PRINTF_FORMAT_SIZE_TYPE,
                              i + 1);
            cx_string_sprintf(comment, "Coordinate of axis %"
                              CX_PRINTF_FORMAT_SIZE_TYPE " at reference "
                              "pixel", i + 1);
            cpl_propertylist_append_double(properties,
                                           cx_string_get(keyword), crval[i]);
            cpl_propertylist_set_comment(properties, cx_string_get(keyword),
                                         cx_string_get(comment));

        }

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

            if (cunit[i] != NULL) {
                cx_string_sprintf(keyword, "CUNIT%-"
                                  CX_PRINTF_FORMAT_SIZE_TYPE, i + 1);
                cx_string_sprintf(comment, "Unit of coordinate axis %"
                                  CX_PRINTF_FORMAT_SIZE_TYPE, i + 1);
                cpl_propertylist_append_string(properties,
                                               cx_string_get(keyword),
                                               cunit[i]);
                cpl_propertylist_set_comment(properties,
                                             cx_string_get(keyword),
                                             cx_string_get(comment));
            }

        }


        /*
         * CD matrix
         */

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

            register cxsize j = 0;


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

                cx_string_sprintf(keyword, "CD%-" CX_PRINTF_FORMAT_SIZE_TYPE
                                  "_%-" CX_PRINTF_FORMAT_SIZE_TYPE, i + 1,
                                  j + 1);
                cx_string_sprintf(comment, "Coordinate transformation matrix "
                                  "element");
                cpl_propertylist_append_double(properties,
                                               cx_string_get(keyword),
                                               cpl_matrix_get(cd, i, j));
                cpl_propertylist_set_comment(properties,
                                             cx_string_get(keyword),
                                             cx_string_get(comment));
            }

        }

        cx_string_delete(keyword);
        keyword = NULL;

        cx_string_delete(comment);
        comment = NULL;

    }

    return 0;

}


cxint
giraffe_frameset_set_groups(cpl_frameset* set, GiGroupInfo *groups)
{

    cpl_frame* frame = NULL;


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

    if (groups == NULL || groups->tag == NULL) {
        return 0;
    }

    frame = cpl_frameset_get_first(set);

    while (frame != NULL) {

        const cxchar* tag = cpl_frame_get_tag(frame);

        if (tag != NULL &&
            cpl_frame_get_group(frame) == CPL_FRAME_GROUP_NONE) {

            const GiGroupInfo* g = groups;

            while (g->tag != NULL) {
                if (strcmp(tag, g->tag) == 0) {
                    cpl_frame_set_group(frame, g->group);
                    break;
                }
                ++g;
            }

        }

        frame = cpl_frameset_get_next(set);

    }

    return 0;

}


/**
 * @brief
 *   Retrieve the read-out noise from the given properties.
 *
 * @param properties  The property list to use for the read-out noise
 *                    computation.
 *
 * @return
 *   The function returns the computed read-out noise in electrons. In case
 *   an error occurred, a negative number is returned, and an appropriate
 *   error code is set.
 *
 * @error
 *   <table class="ec" align="center">
 *     <tr>
 *       <td class="ecl">CPL_ERROR_DATA_NOT_FOUND</td>
 *       <td class="ecr">
 *         Any of the required (detecor read-out noise, conversion factor)
 *         properties was not found.
 *       </td>
 *     </tr>
 *   </table>
 * @enderror
 *
 * The function determines the detector read-out noise in electrons from the
 * properties @em properties. The properties are searched for the measured
 * read-out noise property (bias sigma). If this is not found, the default
 * detector read-out noise property is taken. The conversion factor property
 * is used to convert the read-out noise from ADU into electrons, as necessary.
 */
cxdouble
giraffe_propertylist_get_ron(const cpl_propertylist* properties)
{

    const cxchar* const fctid = "giraffe_propertylist_get_ron";


    cxdouble ron   = 0.;


    cx_assert(properties != NULL);

    if (!cpl_propertylist_has(properties, GIALIAS_BIASSIGMA)) {
        if (!cpl_propertylist_has(properties, GIALIAS_RON)) {
            cpl_msg_error(fctid, "Missing detector read-out noise "
                          "property (%s)!", GIALIAS_RON);
            cpl_error_set(fctid, CPL_ERROR_DATA_NOT_FOUND);

            return 0.;
        }
        else {

            /*
             * Get default detector read-out noise. Note that this value
             * is already given in electrons. No conversion needed.
             */

            cpl_msg_warning(fctid, "Missing bias RMS property (%s)! "
                            "Using detector read-out noise property (%s).",
                            GIALIAS_BIASSIGMA, GIALIAS_RON);
            ron = cpl_propertylist_get_double(properties, GIALIAS_RON);
        }
    }
    else {

        cxdouble conad = 0.;


        /*
         * Get measured detector read-out noise. This is given in ADU and
         * has to be converted into electrons.
         */

        if (!cpl_propertylist_has(properties, GIALIAS_CONAD)) {
            cpl_msg_error(fctid, "Missing detector gain property (%s)! ",
                          GIALIAS_CONAD);
            cpl_error_set(fctid, CPL_ERROR_DATA_NOT_FOUND);

            return 0.;
        }
        else {
            conad = cpl_propertylist_get_double(properties, GIALIAS_CONAD);
        }

        if (conad < 0.) {
            cpl_msg_error(fctid, "Invalid conversion factor (%s) %.3g "
                          "[e-/ADU]", GIALIAS_CONAD, conad);
            cpl_error_set(fctid, CPL_ERROR_ILLEGAL_INPUT);

            return 0.;
        }

        ron = cpl_propertylist_get_double(properties, GIALIAS_BIASSIGMA) *
                conad;

    }

    return ron;

}
/**@}*/
