/*****************************************************************************
 * shared_strings - A library for creating Excel XLSX sst files.
 *
 * Used in conjunction with the libxlsxwriter library.
 *
 * Copyright 2014-2020, John McNamara, jmcnamara@cpan.org. See LICENSE.txt.
 *
 */

#include "xlsxwriter/xmlwriter.h"
#include "xlsxwriter/shared_strings.h"
#include "xlsxwriter/utility.h"
#include <ctype.h>

/*
 * Forward declarations.
 */

STATIC int _element_cmp(struct sst_element *element1,
                        struct sst_element *element2);

#ifndef __clang_analyzer__
LXW_RB_GENERATE_ELEMENT(sst_rb_tree, sst_element, sst_tree_pointers,
                        _element_cmp);
#endif

/*****************************************************************************
 *
 * Private functions.
 *
 ****************************************************************************/

/*
 * Create a new SST SharedString object.
 */
lxw_sst *
lxw_sst_new(void)
{
    /* Create the new shared string table. */
    lxw_sst *sst = calloc(1, sizeof(lxw_sst));
    RETURN_ON_MEM_ERROR(sst, NULL);

    /* Add the sst RB tree. */
    sst->rb_tree = calloc(1, sizeof(struct sst_rb_tree));
    GOTO_LABEL_ON_MEM_ERROR(sst->rb_tree, mem_error);

    /* Add a list for tracking the insertion order. */
    sst->order_list = calloc(1, sizeof(struct sst_order_list));
    GOTO_LABEL_ON_MEM_ERROR(sst->order_list, mem_error);

    /* Initialize the order list. */
    STAILQ_INIT(sst->order_list);

    /* Initialize the RB tree. */
    RB_INIT(sst->rb_tree);

    return sst;

mem_error:
    lxw_sst_free(sst);
    return NULL;
}

/*
 * Free a SST SharedString table object.
 */
void
lxw_sst_free(lxw_sst *sst)
{
    struct sst_element *sst_element;
    struct sst_element *sst_element_temp;

    if (!sst)
        return;

    /* Free the sst_elements and their data using the ordered linked list. */
    if (sst->order_list) {
        STAILQ_FOREACH_SAFE(sst_element, sst->order_list, sst_order_pointers,
                            sst_element_temp) {

            if (sst_element && sst_element->string)
                free(sst_element->string);
            if (sst_element)
                free(sst_element);
        }
    }

    free(sst->order_list);
    free(sst->rb_tree);
    free(sst);
}

/*
 * Comparator for the element structure
 */
STATIC int
_element_cmp(struct sst_element *element1, struct sst_element *element2)
{
    return strcmp(element1->string, element2->string);
}

/*****************************************************************************
 *
 * XML functions.
 *
 ****************************************************************************/
/*
 * Write the XML declaration.
 */
STATIC void
_sst_xml_declaration(lxw_sst *self)
{
    lxw_xml_declaration(self->file);
}

/*
 * Write the <t> element.
 */
STATIC void
_write_t(lxw_sst *self, char *string)
{
    struct xml_attribute_list attributes;
    struct xml_attribute *attribute;

    LXW_INIT_ATTRIBUTES();

    /* Add attribute to preserve leading or trailing whitespace. */
    if (isspace((unsigned char) string[0])
        || isspace((unsigned char) string[strlen(string) - 1]))
        LXW_PUSH_ATTRIBUTES_STR("xml:space", "preserve");

    lxw_xml_data_element(self->file, "t", string, &attributes);

    LXW_FREE_ATTRIBUTES();
}

/*
 * Write the <si> element.
 */
STATIC void
_write_si(lxw_sst *self, char *string)
{
    uint8_t escaped_string = LXW_FALSE;

    lxw_xml_start_tag(self->file, "si", NULL);

    /* Look for and escape control chars in the string. */
    if (lxw_has_control_characters(string)) {
        string = lxw_escape_control_characters(string);
        escaped_string = LXW_TRUE;
    }

    /* Write the t element. */
    _write_t(self, string);

    lxw_xml_end_tag(self->file, "si");

    if (escaped_string)
        free(string);
}

/*
 * Write the <si> element for rich strings.
 */
STATIC void
_write_rich_si(lxw_sst *self, char *string)
{
    lxw_xml_rich_si_element(self->file, string);
}

/*
 * Write the <sst> element.
 */
STATIC void
_write_sst(lxw_sst *self)
{
    struct xml_attribute_list attributes;
    struct xml_attribute *attribute;
    char xmlns[] =
        "http://schemas.openxmlformats.org/spreadsheetml/2006/main";

    LXW_INIT_ATTRIBUTES();
    LXW_PUSH_ATTRIBUTES_STR("xmlns", xmlns);
    LXW_PUSH_ATTRIBUTES_INT("count", self->string_count);
    LXW_PUSH_ATTRIBUTES_INT("uniqueCount", self->unique_count);

    lxw_xml_start_tag(self->file, "sst", &attributes);

    LXW_FREE_ATTRIBUTES();
}

/*****************************************************************************
 *
 * XML file assembly functions.
 *
 ****************************************************************************/

/*
 * Assemble and write the XML file.
 */
STATIC void
_write_sst_strings(lxw_sst *self)
{
    struct sst_element *sst_element;

    STAILQ_FOREACH(sst_element, self->order_list, sst_order_pointers) {
        /* Write the si element. */
        if (sst_element->is_rich_string)
            _write_rich_si(self, sst_element->string);
        else
            _write_si(self, sst_element->string);

    }
}

/*
 * Assemble and write the XML file.
 */
void
lxw_sst_assemble_xml_file(lxw_sst *self)
{
    /* Write the XML declaration. */
    _sst_xml_declaration(self);

    /* Write the sst element. */
    _write_sst(self);

    /* Write the sst strings. */
    _write_sst_strings(self);

    /* Close the sst tag. */
    lxw_xml_end_tag(self->file, "sst");
}

/*****************************************************************************
 *
 * Public functions.
 *
 ****************************************************************************/
/*
 * Add to or find a string in the SST SharedString table and return it's index.
 */
struct sst_element *
lxw_get_sst_index(lxw_sst *sst, const char *string, uint8_t is_rich_string)
{
    struct sst_element *element;
    struct sst_element *existing_element;

    /* Create an sst element to potentially add to the table. */
    element = calloc(1, sizeof(struct sst_element));
    if (!element)
        return NULL;

    /* Create potential new element with the string and its index. */
    element->index = sst->unique_count;
    element->string = lxw_strdup(string);
    element->is_rich_string = is_rich_string;

    /* Try to insert it and see whether we already have that string. */
    existing_element = RB_INSERT(sst_rb_tree, sst->rb_tree, element);

    /* If existing_element is not NULL, then it already existed. */
    /* Free new created element. */
    if (existing_element) {
        free(element->string);
        free(element);
        sst->string_count++;
        return existing_element;
    }

    /* If it didn't exist, also add it to the insertion order linked list. */
    STAILQ_INSERT_TAIL(sst->order_list, element, sst_order_pointers);

    /* Update SST string counts. */
    sst->string_count++;
    sst->unique_count++;
    return element;
}
