/*
    ELAPI

    Module contains functions related to outputting events in CSV format.

    Copyright (C) Dmitri Pal <dpal@redhat.com> 2009

    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 3 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, see <http://www.gnu.org/licenses/>.
*/

#define _GNU_SOURCE
#include <errno.h>      /* for errors */
#include <stdlib.h>     /* for free() */
#include <string.h>     /* for strcmp() */

#include "collection.h"
#include "file_fmt_csv.h"
#include "collection_tools.h"
#include "ini_config.h"
#include "trace.h"
#include "config.h"

/* Reasonable size for one event */
/* FIXME: may be it would make sense to make it configurable ? */
#define FILE_CSV_BLOCK      256

/* Calculate the potential size of the item */
static unsigned file_csv_data_len(struct file_csv_cfg *cfg,
                                  int type,
                                  int raw_len)
{
    int serialized_len = 0;

    TRACE_FLOW_STRING("file_csv_data_len", "Entry point");

    switch (type) {
    case COL_TYPE_INTEGER:
    case COL_TYPE_UNSIGNED:
    case COL_TYPE_LONG:
    case COL_TYPE_ULONG:
        serialized_len = MAX_LONG_STRING_LEN;
        break;

    case COL_TYPE_STRING:
        if ((cfg->csvqualifier) &&
            (cfg->csvescchar)) serialized_len = raw_len * 2;
        else serialized_len = raw_len;
        break;

    case COL_TYPE_BINARY:
        serialized_len = raw_len * 2;
        break;

    case COL_TYPE_DOUBLE:
        serialized_len = MAX_DOUBLE_STRING_LEN;
        break;

    case COL_TYPE_BOOL:
        serialized_len = MAX_BOOL_STRING_LEN;
        break;

    default:
        serialized_len = 0;
        break;
    }

    if (cfg->csvqualifier) serialized_len += 2;

    TRACE_FLOW_STRING("file_csv_data_len", "Exit point");
    return (uint32_t)serialized_len;
}

/* Copy data escaping characters */
int file_copy_esc(char *dest,
                  const char *source,
                  unsigned char what_to_esc,
                  unsigned char what_to_use)
{
    int i = 0;
    int j = 0;

    while (source[i]) {
        if ((source[i] == what_to_use) ||
            (source[i] == what_to_esc)) {

            dest[j] = what_to_use;
            j++;

        }
        dest[j] = source[i];
        i++;
        j++;
    }

    return j;
}

/* Serialize item into the csv format */
int file_serialize_csv(struct elapi_data_out *out_data,
                       int type,
                       int length,
                       void *data,
                       void *mode_cfg)
{
    int error = EOK;
    struct file_csv_cfg *cfg;
    uint32_t projected_len;
    uint32_t used_len;
    int first = 1;
    int i;

    TRACE_FLOW_STRING("file_serialize_csv", "Entry");

    cfg = (struct file_csv_cfg *)mode_cfg;

    /* Get projected length of the item */
    projected_len = file_csv_data_len(cfg, type, length);

    TRACE_INFO_NUMBER("Expected data length: ", projected_len);

    /* Make sure we have enough space */
    if (out_data->buffer != NULL) {
        TRACE_INFO_STRING("Not a first time use.", "Adding length overhead");
        if (cfg->csvseparator) projected_len++;
        projected_len += cfg->csvnumsp;
        first = 0;
    }
    else {
        /* Add null terminating zero */
        projected_len++;
    }

    /* Grow buffer if needed */
    error = elapi_grow_data(out_data,
                            projected_len,
                            FILE_CSV_BLOCK);
    if (error) {
        TRACE_ERROR_NUMBER("Error. Failed to allocate memory.", error);
        return error;
    }

    /* Now everything should fit */
    if (!first) {
        /* Add separator if any */
        if (cfg->csvseparator) {
            out_data->buffer[out_data->length] = cfg->csvseparator;
            out_data->length++;
        }

        /* Add spaces if any */
        memset(&out_data->buffer[out_data->length],
               cfg->csvspace,
               cfg->csvnumsp);
    }

    /* Add qualifier */
    if (cfg->csvqualifier) {
        out_data->buffer[out_data->length] = cfg->csvqualifier;
        out_data->length++;
    }

    /* Add the value */
    switch (type) {
    case COL_TYPE_STRING:

        if ((cfg->csvqualifier) && (cfg->csvescchar)) {
            /* Qualify and escape */
            used_len = file_copy_esc((char *)&out_data->buffer[out_data->length],
                                     (const char *)(data),
                                     cfg->csvqualifier,
                                     cfg->csvescchar);
        }
        else {
            /* No escaping so just copy without trailing 0 */
            /* Item's length includes trailing 0 for data items */
            used_len = length - 1;
            memcpy(&out_data->buffer[out_data->length],
                   (const char *)(data),
                   used_len);
        }
        break;

    case COL_TYPE_BINARY:

        for (i = 0; i < length; i++)
            sprintf((char *)&out_data->buffer[out_data->length + i * 2],
                    "%02X", (unsigned int)(((const unsigned char *)(data))[i]));
        used_len = length * 2;
        break;

    case COL_TYPE_INTEGER:
        used_len = sprintf((char *)&out_data->buffer[out_data->length],
                           "%d", *((const int *)(data)));
        break;

    case COL_TYPE_UNSIGNED:
        used_len = sprintf((char *)&out_data->buffer[out_data->length],
                           "%u", *((const unsigned int *)(data)));
        break;

    case COL_TYPE_LONG:
        used_len = sprintf((char *)&out_data->buffer[out_data->length],
                           "%ld", *((const long *)(data)));
        break;

    case COL_TYPE_ULONG:
        used_len = sprintf((char *)&out_data->buffer[out_data->length],
                           "%lu", *((const unsigned long *)(data)));
        break;

    case COL_TYPE_DOUBLE:
        used_len = sprintf((char *)&out_data->buffer[out_data->length],
                           "%.4f", *((const double *)(data)));
        break;

    case COL_TYPE_BOOL:
        used_len = sprintf((char *)&out_data->buffer[out_data->length],
                           "%s",
                           (*((const unsigned char *)(data))) ? "true" : "false");
        break;

    default:
        out_data->buffer[out_data->length] = '\0';
        used_len = 0;
        break;
    }

    /* Adjust length */
    out_data->length += used_len;

    /* Add qualifier */
    if (cfg->csvqualifier) {
        out_data->buffer[out_data->length] = cfg->csvqualifier;
        out_data->length++;
    }

    /* The "length" member of the structure does not account
     * for the 0 symbol but we made sure that it fits
     * when we asked for the memory at the top.
     */
    out_data->buffer[out_data->length] = '\0';

    TRACE_INFO_STRING("Data: ", (char *)out_data->buffer);

    TRACE_FLOW_STRING("file_serialize_csv.", "Exit");
    return error;

}

/* Function that reads the specific configuration
 * information about the format of the output
 */
int file_get_csv_cfg(void **storage,
                     const char *name,
                     struct collection_item *ini_config,
                     const char *appname)
{
    int error = EOK;
    struct collection_item *cfg_item = NULL;
    struct file_csv_cfg *cfg= NULL;
    const char *qual;
    const char *sep;
    const char *esc;
    const char *space;

    TRACE_FLOW_STRING("file_get_csv_cfg", "Entry");

    /* Allocate memory for configuration */
    cfg = (struct file_csv_cfg *) calloc(1, sizeof(struct file_csv_cfg));
    if (cfg == NULL) {
        TRACE_ERROR_NUMBER("Failed to allocate storage for CSV configuration", ENOMEM);
        return ENOMEM;
    }

    /*********** Qualifier *************/

    /* Get qualifier */
    error = get_config_item(name,
                            FILE_CSV_QUAL,
                            ini_config,
                            &cfg_item);
    if (error) {
        TRACE_ERROR_NUMBER("Attempt to read qualifier attribute returned error", error);
        free(cfg);
        return error;
    }

    /* Do we have qualifier? */
    if (cfg_item == NULL) {
        /* There is no qualifier - use default */
        cfg->csvqualifier = FILE_CSV_DEF_QUAL;
    }
    else {
        /* Get qualifier from configuration */
        error = EOK;
        qual = get_const_string_config_value(cfg_item, &error);
        if (error) {
            TRACE_ERROR_STRING("Failed to get value from configuration.", "Fatal Error!");
            free(cfg);
            return error;
        }

        if (qual[0] == '\0') cfg->csvqualifier = '\0';
        else if(qual[1] != '\0') {
            TRACE_ERROR_STRING("Qualifier has more than one symbol.", "Fatal Error!");
            free(cfg);
            return EINVAL;
        }
        else cfg->csvqualifier = qual[0];
    }

    /*********** Separator *************/

    /* Get separator */
    error = get_config_item(name,
                            FILE_CSV_SEP,
                            ini_config,
                            &cfg_item);
    if (error) {
        TRACE_ERROR_NUMBER("Attempt to read separator attribute returned error", error);
        free(cfg);
        return error;
    }

    /* Do we have separator? */
    if (cfg_item == NULL) {
        /* There is no separator - use default */
        cfg->csvseparator = FILE_CSV_DEF_SEP;
    }
    else {
        /* Get separator from configuration */
        error = EOK;
        sep = get_const_string_config_value(cfg_item, &error);
        if (error) {
            TRACE_ERROR_STRING("Failed to get value from configuration.", "Fatal Error!");
            free(cfg);
            return error;
        }

        if (sep[0] == '\0') cfg->csvseparator = '\0';
        else if(sep[1] != '\0') {
            TRACE_ERROR_STRING("Separator has more than one symbol.", "Fatal Error!");
            free(cfg);
            return EINVAL;
        }
        else cfg->csvseparator = sep[0];
    }

    /*********** Escape symbol *************/

    /* Get escape symbol */
    error = get_config_item(name,
                            FILE_CSV_ESCSYM,
                            ini_config,
                            &cfg_item);
    if (error) {
        TRACE_ERROR_NUMBER("Attempt to read esc symbol attribute returned error", error);
        free(cfg);
        return error;
    }

    /* Do we have esc symbol? */
    if (cfg_item == NULL) {
        /* There is no esc symbol - use default */
        cfg->csvescchar = FILE_CSV_DEF_ESC;
    }
    else {
        /* Get esc symbol from configuration */
        error = EOK;
        esc = get_const_string_config_value(cfg_item, &error);
        if (error) {
            TRACE_ERROR_STRING("Failed to get value from configuration.", "Fatal Error!");
            free(cfg);
            return error;
        }

        if (esc[0] == '\0') cfg->csvescchar = '\0';
        else if(esc[1] != '\0') {
            TRACE_ERROR_STRING("Esc symbol has more than one symbol.", "Fatal Error!");
            free(cfg);
            return EINVAL;
        }
        else cfg->csvescchar = esc[0];
    }

    /*********** Space *************/

    /* Get space */
    error = get_config_item(name,
                            FILE_CSV_SPACE,
                            ini_config,
                            &cfg_item);
    if (error) {
        TRACE_ERROR_NUMBER("Attempt to read space attribute returned error", error);
        free(cfg);
        return error;
    }

    /* Do we have space? */
    if (cfg_item == NULL) {
        /* There is no esc symbol - use default */
        cfg->csvspace = FILE_CSV_DEF_SPC;
    }
    else {
        /* Get file name from configuration */
        error = EOK;
        space = get_const_string_config_value(cfg_item, &error);
        if (error) {
            TRACE_ERROR_STRING("Failed to get value from configuration.", "Fatal Error!");
            free(cfg);
            return error;
        }

        /* Determine what to use as a space symbol */
        if (space[0] == '\0') cfg->csvspace = ' ';
        else if(strcmp(space, FILE_CSV_SP) == 0) cfg->csvspace = ' ';
        else if(strcmp(space, FILE_CSV_TAB) == 0) cfg->csvspace = '\t';
        else if(strcmp(space, FILE_CSV_CR) == 0) cfg->csvspace = '\n';
        else {
            TRACE_ERROR_STRING("Esc symbol has more than one symbol.", "Fatal Error!");
            free(cfg);
            return EINVAL;
        }
    }

    /*********** Number of spaces *************/
    /* Get number of spaces */

    cfg_item = NULL;
    error = get_config_item(name,
                            FILE_CSV_NUMSP,
                            ini_config,
                            &cfg_item);
    if (error) {
        TRACE_ERROR_NUMBER("Attempt to read number of spaces attribute returned error", error);
        free(cfg);
        return error;
    }

    /* Do we have number of spaces? */
    if (cfg_item == NULL) {
        /* There is no attribute - assume default */
        TRACE_INFO_STRING("No attribute.", "Assume no spaces");
        cfg->csvnumsp = 0;
    }
    else {
        cfg->csvnumsp = (uint32_t) get_unsigned_config_value(cfg_item, 1, 0, &error);
        if (error) {
            TRACE_ERROR_STRING("Invalid number of spaces value", "Fatal Error!");
            free(cfg);
            return EINVAL;
        }
        /* Check for right range */
        if (cfg->csvnumsp > FILE_MAXSPACE) {
            TRACE_ERROR_STRING("Too many spaces - not allowed", "Fatal Error!");
            free(cfg);
            return ERANGE;
        }
    }

    /*********** Header *************/
    /* Next is header field */

    cfg_item = NULL;
    error = get_config_item(name,
                            FILE_CSV_HEADER,
                            ini_config,
                            &cfg_item);
    if (error) {
        TRACE_ERROR_NUMBER("Attempt to read header attribute returned error", error);
        free(cfg);
        return error;
    }

    /* Do we have header? */
    if (cfg_item == NULL) {
        /* There is no attribute - assume default */
        TRACE_INFO_STRING("No attribute.", "Assume no header");
        cfg->csvheader = 0;
    }
    else {
        cfg->csvheader = (uint32_t) get_bool_config_value(cfg_item, '\0', &error);
        if (error) {
            TRACE_ERROR_STRING("Invalid csv header value", "Fatal Error!");
            free(cfg);
            return EINVAL;
        }
    }

    *((struct file_csv_cfg **)storage) = cfg;

    TRACE_FLOW_STRING("file_get_csv_cfg", "Entry");
    return error;
}

#ifdef ELAPI_VERBOSE

void file_print_fmt_csv(void *data)
{
    struct file_csv_cfg *cfg;

    cfg = (struct file_csv_cfg *)(data);
    if (cfg == NULL) {
        printf("CSV Configuration is undefined!\n");
        return;
    }

    printf("CSV Configuration:\n");
    printf("  Qualifier: ");
    if (cfg->csvqualifier != '\0') printf("[%c]\n", cfg->csvqualifier);
    else printf("[undefined]\n");

    printf("  Separator: ");
    if (cfg->csvseparator != '\0') printf("[%c]\n", cfg->csvseparator);
    else printf("[undefined]\n");

    printf("  Escape: ");
    if (cfg->csvescchar != '\0') printf("[%c]\n", cfg->csvescchar);
    else printf("[undefined]\n");

    printf("  Space: [%c] [ASCII: %d]\n", cfg->csvspace, (int)(cfg->csvspace));
    printf("  Number of spaces: [%d]\n", cfg->csvnumsp);
    printf("  Header: [%s]\n", ((cfg->csvheader > 0) ? "yes" : "no"));
    printf("CSV Configuration END\n");

}
#endif