/**
 * SPDX-FileCopyrightText: Copyright (C) 2024 ZeldaRET
 * SPDX-License-Identifier: MPL-2.0
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */
#include <assert.h>
#include <ctype.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#include "xml.h"
#include "util.h"

#define copy_out(out, v) memcpy((out), &(v), sizeof(v));

/**
 * Parse a string as an integer.
 *
 * The expected value matches case-insensitive regex `^\s*[-+]?(0x[0-9A-F]+|[0-9]+)\s*$`.
 * The value may be base 10, or base 16 with a (case-insensitive) 0x prefix.
 * Leading and trailing whitespace is ignored.
 */
static int
xml_str_to_int(const char *value)
{
    if (value == NULL || value[0] == '\0')
        goto err;

    bool neg = false;
    int res;

    size_t value_len = strlen(value);
    size_t start;

    // consume initial whitespace
    for (start = 0; start < value_len; start++) {
        if (!isspace(value[start]))
            break;
    }
    // if we consumed the whole string, it was bad
    if (start == value_len)
        goto err;

    // handle sign character if present
    if (value[start] == '+' || value[start] == '-') {
        neg = value[start] == '-';
        start++;
    }

    int base;

    // get absolute value in either base 10 or 16
    if (start + 2 < value_len && value[start + 0] == '0' && tolower(value[start + 1]) == 'x') {
        start += 2;
        base = 16;
    } else {
        base = 10;
    }

    char *str_end;
    res = strtol(&value[start], &str_end, base);
    size_t end = str_end - value;
    if (start == end)
        goto err;

    // consume trailing whitespace
    while (value[end] != '\0') {
        if (!isspace(value[end]))
            goto err;
        end++;
    }

    assert(end == value_len);

    // apply sign
    return neg ? -res : res;
err:
    error("bad int value %s", value);
}

void
xml_parse_int(const char *value, void *out)
{
    int v = xml_str_to_int(value);

    copy_out(out, v);
}

void
xml_parse_uint(const char *value, void *out)
{
    int v = xml_str_to_int(value);
    if (v < 0)
        error("Value should be unsigned");

    copy_out(out, v);
}

void
xml_parse_s16(const char *value, void *out)
{
    int v = xml_str_to_int(value);
    if (v < INT16_MIN || v > INT16_MAX)
        error("Value %d out of range for s16", v);
    int16_t vs16 = v;

    copy_out(out, vs16);
}

void
xml_parse_u8(const char *value, void *out)
{
    int v = xml_str_to_int(value);
    if (v < 0 || v > UINT8_MAX)
        error("Value %d out of range for u8", v);
    uint8_t vu8 = v;

    copy_out(out, vu8);
}

void
xml_parse_s8(const char *value, void *out)
{
    int v = xml_str_to_int(value);
    if (v < INT8_MIN || v > INT8_MAX)
        error("Value %d out of range for s8", v);
    int8_t vs8 = v;

    copy_out(out, vs8);
}

/**
 * Parse a note number name to its s8 [0;127] value.
 * For example "PITCH_EF4" -> 42.
 */
void
xml_parse_note_number(const char *value, void *out)
{
    size_t value_len = strlen(value);
    int8_t vs8;
    char c;

    if (value_len == 0)
        goto err;

    // consume initial whitespace
    size_t start;
    for (start = 0; start < value_len; start++) {
        if (!isspace(value[start]))
            break;
    }
    // if we consumed the whole string, it was bad
    if (start == value_len)
        goto err;

    c = toupper(value[start]);

    if (c >= 'A' && c <= 'G') {
        start++;

        // got a note number
        static const int8_t notes_lut[] = {
            /* A */ 12,
            /* B */ 14,
            /* C */ 3,
            /* D */ 5,
            /* E */ 7,
            /* F */ 8,
            /* G */ 10,
        };
        char cm = toupper(value[start]);
        int mod = 0;
        int v;

        if (value_len > start && (cm == 'F' || cm == 'S')) {
            // handle flat/sharp modifier
            mod = (cm == 'S') ? 1 : -1;
            start++;
        }

        if (start == value_len)
            goto err;

        bool neg = false;
        int res;

        // if value starts with NEG (ignoring case)
        if (start + 3 <= value_len && toupper(value[start + 0]) == 'N' && toupper(value[start + 1]) == 'E' &&
            toupper(value[start + 2]) == 'G') {
            neg = true;
            start += 3;
        }

        int base = 10;

        char *str_end;
        res = strtol(&value[start], &str_end, base);
        size_t end = str_end - value;
        if (start == end)
            goto err;

        // consume trailing whitespace
        while (value[end] != '\0') {
            if (!isspace(value[end]))
                goto err;
            end++;
        }

        assert(end == value_len);

        // apply sign
        v = neg ? -res : res;

        if (v < -1 || v > 10)
            error("Value %d out of range for note number", v);

        vs8 = (v - 1) * 12 + notes_lut[c - 'A'] + mod;
        if (vs8 < 0)
            vs8 += 128;
    } else { // got a raw value
        vs8 = xml_str_to_int(&value[start]);
    }

    if (vs8 < 0)
        goto err;

    copy_out(out, vs8);
    return;
err:
    error("Invalid note %s", value);
}

/**
 * Parse fine tuning in the range [-100, 100]
 */
void
xml_parse_fine_tune(const char *value, void *out)
{
    int v = xml_str_to_int(value);
    if (v < -100 || v > 100)
        error("Value %d out of range for fine tuning", v);
    int8_t vs8 = v;

    copy_out(out, vs8);
}

void
xml_parse_string(const char *value, void *out)
{
    // copies only the pointer to the string
    copy_out(out, value);
}

void
xml_parse_c_identifier(const char *value, void *out)
{
    if (!str_is_c_identifier(value))
        error("Input %s is not a valid C Language identifier", value);

    // copies only the pointer to the string
    copy_out(out, value);
}

void
xml_parse_bool(const char *value, void *out)
{
    bool v;

    // TODO make case-insensitive
    if (strequ(value, "true"))
        v = true;
    else if (strequ(value, "false"))
        v = false;
    else
        error("Invalid value %s for bool", value);

    copy_out(out, v);
}

void
xml_parse_float(const char *value, void *out)
{
    char *end;
    float v = strtof(value, &end);

    if (value == end)
        error("Invalid value %s for float", value);
    if (v < 0.0f)
        error("Only positive floats are allowed");

    copy_out(out, v);
}

void
xml_parse_double(const char *value, void *out)
{
    char *end;
    double v = strtod(value, &end);

    if (value == end)
        error("Invalid value %s for double", value);
    if (v < 0.0)
        error("Only positive doubles are allowed");

    copy_out(out, v);
}

void
xml_get_single_property(void *out, const xmlNodePtr node, const char *name, xml_parser_func parser)
{
    xmlAttrPtr attr = node->properties;

    if (attr == NULL || attr->next != NULL)
        error("Expected only property %s on line %d", name, node->line);

    const char *prop_name = XMLSTR_TO_STR(attr->name);

    if (!strequ(prop_name, name))
        error("Unexpected attribute on line %d: got: \"%s\", expected: \"%s\"", node->line, prop_name, name);

    xmlChar *xvalue = xmlNodeListGetString(node->doc, attr->children, 1);
    const char *value = XMLSTR_TO_STR(xvalue);

    parser(value, (uint8_t *)out);

    if (parser != xml_parse_string && parser != xml_parse_c_identifier)
        free(xvalue);
}

void
xml_parse_node_by_spec(void *out, const xmlNodePtr node, const xml_attr_spec spec, size_t spec_length)
{
    bool *got = alloca(spec_length * sizeof(bool));
    memset(got, false, spec_length * sizeof(bool));

    LL_FOREACH(xmlAttrPtr, attr, node->properties) {
        const char *name = XMLSTR_TO_STR(attr->name);
        bool found = false;

        for (size_t i = 0; i < spec_length; i++) {
            if (strequ(name, spec[i].name)) {
                // strictly speaking this pointer needs to be freed but we may want to save the string itself
                // so we just don't free it and let it clean up when the program terminates
                xmlChar *xvalue = xmlNodeListGetString(node->doc, attr->children, 1);
                const char *value = XMLSTR_TO_STR(xvalue);

                spec[i].parser_func(value, (uint8_t *)out + spec[i].offset);

                if (spec[i].parser_func != xml_parse_string && spec[i].parser_func != xml_parse_c_identifier)
                    free(xvalue); // free when we don't need the string in the future, TODO strdup when we do need it?

                got[i] = true;
                found = true;
                break;
            }
        }

        if (!found)
            error("Unrecognized attribute %s on line %d", name, node->line);
    }

    for (size_t i = 0; i < spec_length; i++) {
        if (!spec[i].optional && !got[i])
            error("Expected a %s attribute on line %d", spec[i].name, node->line);
    }
}

static void
xml_print_rec(xmlNodePtr base, int *pIndent)
{
    LL_FOREACH(xmlNodePtr, node, base) {
        if (node->type != XML_ELEMENT_NODE)
            continue;

        fprintf(stdout, "%*cChild is <%s> (%i)\n", *pIndent, ' ', node->name, node->type);
        *pIndent += 4;

        LL_FOREACH(xmlAttrPtr, attr, node->properties) {
            xmlChar *value = xmlNodeListGetString(node->doc, attr->children, 1);
            fprintf(stdout, "%*c- Property <%s> \"%s\"\n", *pIndent, ' ', attr->name, XMLSTR_TO_STR(value));
            free(value);
        }

        xml_print_rec(node->children, pIndent);
        *pIndent -= 4;
    }
}

void
xml_print_tree(xmlDocPtr document)
{
    xmlNodePtr root = xmlDocGetRootElement(document);
    int indent = 4;
    fprintf(stdout, "Root is <%s> (%i)\n", root->name, root->type);

    LL_FOREACH(xmlAttrPtr, attr, root->properties) {
        xmlChar *value = xmlNodeListGetString(root->doc, attr->children, 1);
        fprintf(stdout, "%*c- Property <%s> \"%s\"\n", indent, ' ', attr->name, XMLSTR_TO_STR(value));
        free(value);
    }

    xml_print_rec(root->children, &indent);
}
