/* $Id: fors_paf.c,v 1.6 2013-08-14 13:28:56 cgarcia Exp $
 *
 * This file is part of the FORS Library
 * Copyright (C) 2002-2010 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: cgarcia $
 * $Date: 2013-08-14 13:28:56 $
 * $Revision: 1.6 $
 * $Name: not supported by cvs2svn $
 */

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

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <pwd.h>
#include <sys/types.h>
#include <time.h>
#include <assert.h>

#include <cpl.h>
#include <fors_paf.h>


/*
 * Reserved PAF header keywords
 */

#define PAF_HDR_START      "PAF.HDR.START"
#define PAF_TYPE           "PAF.TYPE"
#define PAF_ID             "PAF.ID"
#define PAF_NAME           "PAF.NAME"
#define PAF_DESC           "PAF.DESC"
#define PAF_CRTE_NAME      "PAF.CRTE.NAME"
#define PAF_CRTE_TIME      "PAF.CRTE.DAYTIM"
#define PAF_LCHG_NAME      "PAF.LCHG.NAME"
#define PAF_LCHG_TIME      "PAF.LCHG.DAYTIM"
#define PAF_CHCK_NAME      "PAF.CHCK.NAME"
#define PAF_CHCK_TIME      "PAF.CHCK.DAYTIM"
#define PAF_CHCK_CHECKSUM  "PAF.CHCK.CHECKSUM"
#define PAF_HDR_END        "PAF.HDR.END"


/*
 * Value and comment field start position
 */

#define PAF_FIELD_OFFSET_VALUE    20
#define PAF_FIELD_OFFSET_COMMENT  45


/** 
 * @defgroup forsPaf PAF File Utilities
 *
 * TBD
 */

/**@{*/

/*
 * PAF record definition. This corresponds to one line of a parameter file.
 */

struct _FORS_PAF_RECORD_ {
    char *name;
    char *comment;
    ForsPAFType type;

    union {
        int *bval;
        int *ival;
        double *dval;
        char *sval;
    } data;
};

typedef struct _FORS_PAF_RECORD_ ForsPAFRecord;


/*
 * The PAF object
 */

struct _FORS_PAF_ {
    char *name;
    int nh;
    int nr;
    ForsPAFRecord **header;
    ForsPAFRecord **records;
};


/*
 * @brief
 *   Get current date and time in ISO8601 format.
 *
 * @return Pointer to a statically allocated string in the function
 *   (no need to free it). In case of failure, returns a @c NULL.
 *
 * This private function just returns the current time in ISO8601 format.
 */

#define TIME_ISO8601_LENGTH (20)

static char *getTimeISO8601(void)
{

    static char timeISO8601[TIME_ISO8601_LENGTH];
    time_t      seconds = time((time_t *)0);

    if (strftime(timeISO8601, TIME_ISO8601_LENGTH,
                    "%Y-%m-%dT%T", localtime(&seconds)) == 0)
        strcpy(timeISO8601, "0000-00-00T00:00:00");

    return timeISO8601;

}


/*
 * Compute record type size in bytes.
 */

inline static size_t
_forsPAFValueSize(ForsPAFType type, const void *value)
{
    size_t sz;

    switch (type) {
        case PAF_TYPE_BOOL:
            sz = sizeof(int);
            break;
            
        case PAF_TYPE_INT:
            sz = sizeof(int);
            break;

        case PAF_TYPE_DOUBLE:
            sz = sizeof(double);
            break;

        case PAF_TYPE_STRING:
            sz = (strlen((char *)value) + 1) * sizeof(char);
            break;

        default:
                sz = 0;
                break;
        }

    return sz;

}


/*
 * Destroy a PAF record
 */

inline static void
_forsPAFRecordDestroy(ForsPAFRecord *record)
{

    if (record) {
        cpl_free(record->name);
        cpl_free((void *)record->data.sval);
        cpl_free(record->comment);
        cpl_free(record);
    }

    return;

}


/*
 * Create a new PAF record
 */

inline static ForsPAFRecord *
_forsPAFRecordCreate(const char *name, ForsPAFType type, const void *value,
                    const char *comment)
{

    size_t sz;

    ForsPAFRecord *record = cpl_malloc(sizeof(ForsPAFRecord));


    record->name = cpl_strdup(name);
    record->comment = comment ? cpl_strdup(comment) : NULL;
    record->type = type;

    sz = _forsPAFValueSize(type, value);

    if (sz == 0) {
        record->data.sval = NULL;
    }
    else {
        record->data.sval = (char *)cpl_malloc(sz);
    }

    memcpy(record->data.sval, value, sz);

    return record;

}

/*
 * Set name, value and comment of a PAF record
 */

inline static void
_forsPAFRecordSet(ForsPAFRecord *record, const char *name, ForsPAFType type,
                 const void *value, const char *comment)
{

    if (name) {
        cpl_free(record->name);
        record->name = cpl_strdup(name);
    }

    if (comment) {
        cpl_free(record->comment);
        record->comment = cpl_strdup(comment);
    }

    if (value) {
        size_t sz = _forsPAFValueSize(type, value);

        if (record->data.sval) {
            size_t size = _forsPAFValueSize(record->type, record->data.sval);

            if (sz != size)
                record->data.sval = (char *)cpl_realloc(record->data.sval, sz);
        }
        else
            record->data.sval = (char *)cpl_malloc(sz);

        memcpy(record->data.sval, value, sz);
        record->type = type;
    }

    return;

}


/*
 * Create a record from name, value and comment and append the record to
 * the PAF header or record list.
 */

inline static int
_forsPAFAppend(ForsPAFRecord ***list, int *pos, const char *name, 
              ForsPAFType type, const void *value, const char *comment)
{

    ForsPAFRecord *record;


    record = _forsPAFRecordCreate(name, type, value, comment);
    if (!record)
        return 1;

    if (pos[0] == 0) {
        *list = cpl_malloc(sizeof(ForsPAFRecord *));
    }
    else {
        *list = cpl_realloc(*list, (pos[0]+1) * sizeof(ForsPAFRecord *));
    }

    (*list)[pos[0]] = record;
    pos[0]++;

    return 0;

}


/*
 * Create a new PAF header.
 */

inline static ForsPAFRecord **
_forsPAFHeaderCreate(const char *name, const char *type, const char *id,
                    const char *desc, int *pos)
{

    ForsPAFRecord **hdr;
    const char *user, *timestamp;
#if defined HAVE_GETUID && defined HAVE_GETPWUID
    struct passwd *pw;
#endif

    /* Get user id */

#if defined HAVE_GETUID && defined HAVE_GETPWUID
    pw = getpwuid(getuid());

    if (!pw)
        return NULL;

    user = pw->pw_name;
#else
    user = getenv("USER");
    user = user == NULL ? getenv("LOGNAME") : user;

    if (!user)
    {
        return NULL;
    }
#endif

    /* Get timestamp */

    timestamp = getTimeISO8601();

    pos[0] = 0;

    _forsPAFAppend(&hdr, pos, PAF_HDR_START, PAF_TYPE_NONE, NULL, NULL);
    _forsPAFAppend(&hdr, pos, PAF_TYPE, PAF_TYPE_STRING, type,
                  "Type of parameter file");

    if (id) {
        _forsPAFAppend(&hdr, pos, PAF_ID, PAF_TYPE_STRING, id, NULL);
    }
    else {
        _forsPAFAppend(&hdr, pos, PAF_ID, PAF_TYPE_STRING, "", NULL);
    }

    _forsPAFAppend(&hdr, pos, PAF_NAME, PAF_TYPE_STRING, name, "Name of PAF");

    if (desc)
        _forsPAFAppend(&hdr, pos, PAF_DESC, PAF_TYPE_STRING, desc,
                      "Short description of PAF");
    else
        _forsPAFAppend(&hdr, pos, PAF_DESC, PAF_TYPE_STRING, "",
                      "Short description of PAF");

    _forsPAFAppend(&hdr, pos, PAF_CRTE_NAME, PAF_TYPE_STRING, user,
                  "Name of creator");
    _forsPAFAppend(&hdr, pos, PAF_CRTE_TIME, PAF_TYPE_STRING, timestamp,
                  "Civil time for creation");
    _forsPAFAppend(&hdr, pos, PAF_LCHG_NAME, PAF_TYPE_STRING, user,
                  "Author of par. file");
    _forsPAFAppend(&hdr, pos, PAF_LCHG_TIME, PAF_TYPE_STRING, timestamp,
                  "Timestamp for last change");
    _forsPAFAppend(&hdr, pos, PAF_CHCK_NAME, PAF_TYPE_STRING, "",
                  "Name of appl. checking");
    _forsPAFAppend(&hdr, pos, PAF_CHCK_TIME, PAF_TYPE_STRING, "",
                  "Time for checking");
    _forsPAFAppend(&hdr, pos, PAF_CHCK_CHECKSUM, PAF_TYPE_STRING, "",
                  "Checksum for the PAF");
    _forsPAFAppend(&hdr, pos, PAF_HDR_END, PAF_TYPE_NONE, NULL, NULL);

    return hdr;

}


/*
 * Format a record so that it can be written to a parameter file on disk.
 * The formatted record is written to the given output buffer.
 */

inline static const char *
_forsPAFFormatRecord(ForsPAFRecord *record)
{

    static char buffer[PAF_RECORD_MAX + 1];
    char value[PAF_RECORD_MAX + 1];

    int pos, sz;


    memset(buffer, ' ', PAF_RECORD_MAX);


    /*
     * Verify that the record name fits into the buffer. The extra
     * character is for the semicolon which has to be present.
     */

    if (strlen(record->name) + 1 > PAF_RECORD_MAX)
        return NULL;


    /*
     * Build the formatted string from the record structure
     */

    sz = strlen(record->name);
    strncpy(buffer, record->name, sz);

    pos = sz;
    if (record->data.sval) {
        if (pos < PAF_FIELD_OFFSET_VALUE)
            pos = PAF_FIELD_OFFSET_VALUE;
        else
            pos++;

        switch (record->type) {
            case PAF_TYPE_BOOL:
                snprintf(value, PAF_RECORD_MAX, "%c",
                         *record->data.bval ? 'T' : 'F');
                break;

            case PAF_TYPE_INT:
                snprintf(value, PAF_RECORD_MAX, "%d", *record->data.ival);
                break;

            case PAF_TYPE_DOUBLE:
                snprintf(value, PAF_RECORD_MAX, "%.15G", *record->data.dval);
                if (!strchr(value, '.')) {
                    if (strchr(value, 'E'))
                        snprintf(value, PAF_RECORD_MAX, "%.1E",
                                 *record->data.dval);
                    else
                        strcat(value, ".");
                }
                break;

            case PAF_TYPE_STRING:
                snprintf(value, PAF_RECORD_MAX, "\"%s\"", record->data.sval);
                break;

            case PAF_TYPE_NONE:

                /* 
                 * Should not reach this point. If type is PAF_TYPE_NONE
                 * the data pointer should always be NULL.
                 */
                
                break;
        }

        sz = strlen(value);

        /* 
         * Verify that writing the value string does not overflow the buffer.
         */
        
        if (sz > PAF_RECORD_MAX - pos + 1)
            return NULL;

        strncpy(&buffer[pos], value, sz);
        pos += sz;
    }

    buffer[pos++] = ';';


    /*
     * Comments are not printed if there is room in the buffer for at least 3
     * characters, so that not only the hash and/or the following blank
     * could be stored because of the finite record size.
     */

    if (record->comment && (PAF_RECORD_MAX - pos) >= 2) {
        if (pos < PAF_FIELD_OFFSET_COMMENT)
            pos = PAF_FIELD_OFFSET_COMMENT;
        else
            pos++;

        strncpy(&buffer[pos], "# ", 2);
        pos += 2;
        sz = strlen(record->comment);
        strncpy(&buffer[pos], record->comment, sz);
        pos += sz;
    }

    buffer[pos] = '\0';
    
    return buffer;
}


/**
 * @brief
 *   Destroy a PAF object.
 *
 * @param paf  PAF object to destroy.
 *
 * @return Nothing.
 *
 * The function deallocates all memory used for the PAF object @em paf.
 */

inline void deleteForsPAF(ForsPAF *paf)
{

    int i;

    if (paf) {
        for (i = 0; i < paf->nh; i++)
            _forsPAFRecordDestroy(paf->header[i]);
        for (i = 0; i < paf->nr; i++)
            _forsPAFRecordDestroy(paf->records[i]);
        cpl_free(paf->header);
        cpl_free(paf->records);
        cpl_free(paf->name);
        cpl_free(paf);
    }

    return;

}


/**
 * @brief
 *   Create a new PAF object.
 *
 * @param name  Parameter file name.
 * @param type  Parameter file type.
 * @param id    Parameter file identifier string.
 * @param desc  Short description for the parameter file.
 *
 * @return The handle for the newly created PAF object.
 *
 * The function allocates the memory for a PAF object and initializes the
 * PAF object with the strings @em name, @em type, @em id and @em desc passed
 * to the function, where @em id and @em desc may be omitted, i.e. @c NULL
 * maybe passed for @em id and/or @em desc. The argument @em name will be
 * used as the file name in case the PAF object is written to a disk file.
 */

ForsPAF *newForsPAF(const char *name, const char *type, const char *id,
                  const char *desc)
{

    ForsPAF *paf;
    int     pos = 0;


    if (!name || !type)
        return NULL;

    paf = (ForsPAF *)cpl_malloc(sizeof(ForsPAF));
    if (paf) {
        paf->header = _forsPAFHeaderCreate(name, type, id, desc, &pos);
        if(paf->header == NULL)
        {
            cpl_free(paf);
            return NULL;
        }
        paf->records = NULL;
        paf->nh = pos;
        paf->nr = 0;
        paf->name = cpl_strdup(name);
    }
    
    return paf;

}


/**
 * @brief
 *   Check whether a PAF object is empty.
 *
 * @param paf  PAF object to query.
 *
 * @return The function returns 1 if @em paf is empty, otherwise the return
 *   value is 0.
 *
 * The function checks whether the record list of the PAF object @em paf
 * contains any records.
 */

int
forsPAFIsEmpty(const ForsPAF *paf)
{

    assert(paf != NULL);

    return paf->nr == 0 ? 1 : 0;

}


/**
 * @brief
 *   Get the actual size of the given PAF object.
 *
 * @param paf  PAF object to query.
 *
 * @return The number of PAF records currently stored in the record list, or
 *   0 if the PAF object is empty.
 *
 * The function computes the actual size, i.e. the number of parmeter records,
 * stored in the record list of the PAF object @em paf. Header entries do not
 * contribute the size of a PAF object.
 */

size_t forsPAFGetSize(const ForsPAF *paf)
{
    assert(paf != NULL);

    return (size_t)paf->nr;

}


/**
 * @brief
 *   Verify that the given string is a valid PAF keyword.
 *
 * @param name  Name string to verify.
 *
 * @return The function returns 1 if the given name is valid, if not 0 is
 *   returned.
 *
 * The function does a simple check on the given string. To be a valid PAF
 * keyword the name should start with a letter, it should only contain
 * upper case letters and it may not contain blanks.
 */

inline int
forsPAFIsValidName(const char *name)
{

    register size_t i, sz;


    assert(name != NULL);

    if (strchr(name, ' '))
        return 0;

    sz = strlen(name);
    for (i = 0; i <sz; i++) {
        char c = name[i];

        /*
         * Names may be composed from uppercase letters, digits, the dot
         * and the underscore only.
         */

        /*
         * Note: The characer class functions have to be enclosed in
         *   parantheses to use the actual function on HP-UX where these
         *   functions are also provided as macros, which are taken by
         *   default and may lead to compiler warnings.
         */

        if (!(isupper)(c) && !(isdigit)(c) && c != '.' && c != '_' && c != '-')
            return 0;
    }

    return 1;

}


/**
 * @brief
 *   Append a boolean value to a PAF object.
 *
 * @param paf      PAF object to which the record is appended.
 * @param name     Parameter name.
 * @param value    Parameter value.
 * @param comment  Parameter description.
 *
 * @return The function returns @c EXIT_SUCCESS if the record was
 *   successfully appended, or @c EXIT_FAILURE otherwise.
 *
 * The function creates a new PAF record for the given name, value and comment
 * and appends it to the record list of the PAF object @em paf.
 */

inline int
forsPAFAppendBool(ForsPAF *paf, const char *name, int value, const char *comment)
{
    assert(paf != NULL);
    assert(name != NULL);
    
    if (!forsPAFIsValidName(name) && name[0] != '#' && name [0] != '\0')
        return EXIT_FAILURE;

    if (_forsPAFAppend(&(paf->records), &(paf->nr), name, PAF_TYPE_BOOL, 
                      &value, comment))
        return EXIT_FAILURE;

    return EXIT_SUCCESS;

}


/**
 * @brief
 *   Append a integer value to a PAF object.
 *
 * @param paf      PAF object to which the record is appended.
 * @param name     Parameter name.
 * @param value    Parameter value.
 * @param comment  Parameter description.
 *
 * @return The function returns @c EXIT_SUCCESS if the record was
 *   successfully appended, or @c EXIT_FAILURE otherwise.
 *
 * The function creates a new PAF record for the given name, value and comment
 * and appends it to the record list of the PAF object @em paf.
 */

inline int
forsPAFAppendInt(ForsPAF *paf, const char *name, int value, const char *comment)
{

    assert(paf != NULL);
    assert(name != NULL);
    
    if (!forsPAFIsValidName(name) && name[0] != '#' && name [0] != '\0')
        return EXIT_FAILURE;

    if (_forsPAFAppend(&(paf->records), &(paf->nr), name, PAF_TYPE_INT, 
                      &value, comment))
        return EXIT_FAILURE;

    return EXIT_SUCCESS;

}


/**
 * @brief
 *   Append a double value to a PAF object.
 *
 * @param paf      PAF object to which the record is appended.
 * @param name     Parameter name.
 * @param value    Parameter value.
 * @param comment  Parameter description.
 *
 * @return The function returns @c EXIT_SUCCESS if the record was
 *   successfully appended, or @c EXIT_FAILURE otherwise.
 *
 * The function creates a new PAF record for the given name, value and comment
 * and appends it to the record list of the PAF object @em paf.
 */

inline int
forsPAFAppendDouble(ForsPAF *paf, const char *name, double value,
                   const char *comment)
{

    assert(paf != NULL);
    assert(name != NULL);
    
    if (!forsPAFIsValidName(name) && name[0] != '#' && name [0] != '\0')
        return EXIT_FAILURE;

    if (_forsPAFAppend(&(paf->records), &(paf->nr), name, PAF_TYPE_DOUBLE, 
                      &value, comment))
        return EXIT_FAILURE;

    return EXIT_SUCCESS;

}


/**
 * @brief
 *   Append a string value to a PAF object.
 *
 * @param paf      PAF object to which the record is appended.
 * @param name     Parameter name.
 * @param value    Parameter value string.
 * @param comment  Parameter description.
 *
 * @return The function returns @c EXIT_SUCCESS if the record was
 *   successfully appended, or @c EXIT_FAILURE otherwise.
 *
 * The function creates a new PAF record for the given name, value and comment
 * and appends it to the record list of the PAF object @em paf.
 */

inline int
forsPAFAppendString(ForsPAF *paf, const char *name, const char *value,
                   const char *comment)
{

    assert(paf != NULL);
    assert(name != NULL);
    
    if (!forsPAFIsValidName(name) && name[0] != '#' && name [0] != '\0')
        return EXIT_FAILURE;

    if (_forsPAFAppend(&(paf->records), &(paf->nr), name, PAF_TYPE_STRING, 
                      value, comment))
        return EXIT_FAILURE;

    return EXIT_SUCCESS;

}


/**
 * @brief
 *   Write a PAF object to a disk file.
 *
 * @param paf  PAF object to save.
 *
 * @return The function returns @c EXIT_SUCCESS if the object was
 *   successfully written to the file, otherwise @c EXIT_FAILURE is
 *   returned.
 *
 * The function converts the PAF object into an ASCII file on disk, using
 * the name used to create the PAF object as a file name for the disk file.
 */

int
forsPAFWrite(ForsPAF *paf)
{

    const char *record;
    FILE *stream;
    int i;


    if (!paf)
        return EXIT_FAILURE;

    assert(paf->header != NULL);


    /*
     * Create output file
     */

    stream = fopen(paf->name, "wb");
    if (!stream)
        return EXIT_FAILURE;


    for (i = 0; i < paf->nh; i++) {
        record = _forsPAFFormatRecord(paf->header[i]);
        if (!record) {
            fclose(stream);
            return EXIT_FAILURE;
        }
            
        fprintf(stream, "%s\n", record);
    }


    if (paf->nr) {
        char buffer[PAF_RECORD_MAX];

        buffer[0] = '#';
        memset(&buffer[1], '-', 78);
        buffer[79] = '\0';
        fprintf(stream, "%s\n", buffer);
    }

    for (i = 0; i < paf->nr; i++) {
        record = _forsPAFFormatRecord(paf->records[i]);
        if (!record) {
            fclose(stream);
            return EXIT_FAILURE;
        }

        fprintf(stream, "%s\n", record);
    }

    fclose(stream);

    return EXIT_SUCCESS;
    
}
/**@}*/
