// SPDX-License-Identifier: GPL-3.0-or-later

#include "windows-events-xml.h"

#include <string.h>
#include <stdio.h>

#define INDENT_STEP 2
#define A_LOT_OF_SPACES "                                                                                            "

// Helper: Add indentation
static inline void buffer_add_xml_indent(BUFFER *buffer, const int level) {
    size_t total_spaces = (size_t)level * INDENT_STEP;
    const size_t step = sizeof(A_LOT_OF_SPACES) - 1;
    while (total_spaces > 0) {
        const size_t spaces_to_add = (total_spaces > step) ? step : total_spaces;
        buffer_fast_strcat(buffer, A_LOT_OF_SPACES, spaces_to_add);
        total_spaces -= spaces_to_add;
    }
}

const char *append_the_rest(BUFFER *buffer, const char *xml, const char *end) {
    if(xml >= end) return end;
    buffer_fast_strcat(buffer, xml, end - xml);
    return end;
}

static const char *parse_node(BUFFER *buffer, const char *xml, const char *end, int level);

static const char *find_string_in_slice(const char *start, const char *end, const char *needle, size_t needle_len) {
    if(!needle_len)
        return start;

    if(!start || !end || start >= end || (size_t)(end - start) < needle_len)
        return NULL;

    for(const char *p = start; (size_t)(end - p) >= needle_len; p++) {
        if(*p == *needle && memcmp(p, needle, needle_len) == 0)
            return p;
    }

    return NULL;
}

static const char *find_char_in_slice(const char *start, const char *end, char c) {
    if(!start || !end || start >= end)
        return NULL;

    return memchr(start, c, end - start);
}

// Helper: Parse the value (between > and <) and return the next position to parse
const char *parse_value_and_closing_tag(BUFFER *buffer, const char *xml, const char *end, int level) {
    const char *start = xml;
    bool has_subnodes = false;

    // const char *tag_start = NULL, *tag_end = NULL;
    while (xml < end) {
        if(*xml == '<') {
            if((size_t)(end - xml) > 1 && *(xml + 1) == '/') {
                // a closing tag
                xml += 2;

//                tag_start = xml;

                while(xml < end && *xml != '>')
                    xml++;

//                tag_end = xml;

                if(xml < end && *xml == '>')
                    xml++;

                if(has_subnodes) {
                    buffer_putc(buffer, '\n');
                    buffer_add_xml_indent(buffer, level);
                }

                buffer_fast_strcat(buffer, start, xml - start);
                return xml;
            }
            else {
                // an opening tag
                buffer_fast_strcat(buffer, start, xml - start);
                xml = start = parse_node(buffer, xml, end, level + 1);
                while(xml < end && isspace((uint8_t)*xml))
                    xml++;
                has_subnodes = true;
            }
        }
        else
            xml++;
    }

    return append_the_rest(buffer, start, end);
}

// Parse a field value and return the next position to parse
const char *parse_field_value(BUFFER *buffer, const char *xml, const char *end) {
    const char quote = *xml;

    if(quote != '"' && quote != '\'')
        return append_the_rest(buffer, xml, end);

    const char *start = xml++;

    while (xml < end && *xml != quote) {
        if (*xml == '\\') {
            xml++;  // Skip escape character

            if(xml < end)
                xml++;

            continue;
        }

        xml++;
    }

    if(xml < end && *xml == quote) {
        xml++;  // Move past the closing quote
        buffer_fast_strcat(buffer, start, xml - start);
        return xml;
    }

    return append_the_rest(buffer, start, end);
}

// Parse a field name and return the next position to parse
const char *parse_field(BUFFER *buffer, const char *xml, const char *end) {
    while(xml < end && isspace((uint8_t)*xml)) xml++;

    const char *start = xml;

    while (xml < end && *xml != '=')
        xml++;

    // Append the field name
    buffer_fast_strcat(buffer, start, xml - start);

    if(xml < end && *xml == '=') {
        xml++;

        buffer_putc(buffer, '=');

        if(xml < end && (*xml == '"' || *xml == '\''))
            xml = parse_field_value(buffer, xml, end);

        return xml;  // Return the next character to parse
    }

    return append_the_rest(buffer, start, end);
}

// Parse a node (handles fields and subnodes) and return the next position to parse
static inline const char *parse_node(BUFFER *buffer, const char *xml, const char *end, int level) {
    if(xml >= end || *xml != '<')
        return append_the_rest(buffer, xml, end);

    const char *start = xml++; // skip the <

    buffer_putc(buffer, '\n');
    buffer_add_xml_indent(buffer, level);

    // skip spaces before the tag name
    while(xml < end && isspace((uint8_t)*xml)) xml++;

    // Parse the tag name
//    const char *tag_start = xml, *tag_end = NULL;
    while (xml < end && *xml != '>' && *xml != '/') {
        xml++;

        if(xml < end && isspace((uint8_t)*xml)) {
            xml++;
//            tag_end = xml;

            while(xml < end && isspace((uint8_t)*xml))
                xml++;

            if(xml < end && *xml == '/') {
               // an opening tag that is self-closing
                xml++;
                if(xml < end && *xml == '>') {
                    xml++;
                    buffer_fast_strcat(buffer, start, xml - start);
                    return xml;
                }
                else
                    return append_the_rest(buffer, start, end);
            }
            else if(xml < end && *xml == '>') {
                // the end of an opening tag
                xml++;
                buffer_fast_strcat(buffer, start, xml - start);
                return parse_value_and_closing_tag(buffer, xml, end, level);
            }
            else {
                buffer_fast_strcat(buffer, start, xml - start);
                xml = start = parse_field(buffer, xml, end);
                while(xml < end && isspace((uint8_t)*xml))
                    xml++;
            }
        }
    }

    bool self_closing_tag = false;
    if(xml < end && *xml == '/') {
        self_closing_tag = true;
        xml++;
    }

    if(xml < end && *xml == '>') {
        xml++;
        buffer_fast_strcat(buffer, start, xml - start);

        if(self_closing_tag)
            return xml;

        return parse_value_and_closing_tag(buffer, xml, end, level);
    }

    return append_the_rest(buffer, start, end);
}

static inline void buffer_pretty_print_xml_object(BUFFER *buffer, const char *xml, const char *end) {
    while(xml < end) {
        while(xml < end && isspace((uint8_t)*xml))
            xml++;

        if(xml < end && *xml == '<')
            xml = parse_node(buffer, xml, end, 1);
        else {
            append_the_rest(buffer, xml, end);
            return;
        }
    }
}

void buffer_pretty_print_xml(BUFFER *buffer, const char *xml, size_t xml_len) {
    if(!xml || !xml_len)
        return;

    const char *end = xml + xml_len;
    buffer_pretty_print_xml_object(buffer, xml, end);
}

// --------------------------------------------------------------------------------------------------------------------

bool buffer_extract_and_print_xml_with_cb(BUFFER *buffer, const char *xml, size_t xml_len, const char *prefix, const char *keys[],
                                          void (*cb)(BUFFER *, const char *, const char *, const char *)) {
    if(!xml)
        return false;

    const char *limit = xml + xml_len;

    if(!keys || !keys[0] || !*keys[0]) {
        buffer_pretty_print_xml(buffer, xml, xml_len);
        return true;
    }

    const char *start = xml, *end = NULL;
    for(size_t k = 0; keys[k] ; k++) {
        if(!*keys[k]) continue;

        size_t klen = strlen(keys[k]);
        const char *search_end = end ? end : limit;
        char *tag_open = mallocz(klen + 2);
        tag_open[0] = '<';
        strcpy(&tag_open[1], keys[k]);
        tag_open[klen + 1] = '\0';

        const char *new_start = find_string_in_slice(start, search_end, tag_open, klen + 1);
        freez(tag_open);
        if(!new_start)
            return false;

        start = new_start + klen + 1;
        if(start >= search_end)
            return false;

        if(*start != '>' && !isspace((uint8_t)*start))
            return false;

        if(*start != '>') {
            start = find_char_in_slice(start, search_end, '>');
            if(!start) return false;
        }
        start++; // skip the >
        if(start > search_end)
            return false;

        char *tag_close = mallocz(klen + 4);
        tag_close[0] = '<';
        tag_close[1] = '/';
        strcpy(&tag_close[2], keys[k]);
        tag_close[klen + 2] = '>';
        tag_close[klen + 3] = '\0';

        const char *new_end = find_string_in_slice(start, search_end, tag_close, klen + 3);
        freez(tag_close);
        if(!new_end)
            return false;

        end = new_end;
    }

    if(!start || !end || start == end)
        return false;

    cb(buffer, prefix, start, end);
    return true;
}

static void print_xml_cb(BUFFER *buffer, const char *prefix, const char *start, const char *end) {
    if(prefix)
        buffer_strcat(buffer, prefix);

    buffer_pretty_print_xml_object(buffer, start, end);
}

bool buffer_extract_and_print_xml(BUFFER *buffer, const char *xml, size_t xml_len, const char *prefix, const char *keys[]) {
    return buffer_extract_and_print_xml_with_cb(
        buffer, xml, xml_len,
        prefix, keys,
        print_xml_cb);
}

static void print_value_cb(BUFFER *buffer, const char *prefix, const char *start, const char *end) {
    if(prefix)
        buffer_strcat(buffer, prefix);

    buffer_need_bytes(buffer, end - start + 1);

    char *started = &buffer->buffer[buffer->len];
    char *d = started;
    const char *s = start;

    while(s && s < end) {
        size_t remaining = end - s;
        if(*s == '&' && remaining > 3) {
            if(*(s + 1) == '#') {
                if(remaining > 4 && *(s + 2) == '1' && *(s + 4) == ';') {
                    if (*(s + 3) == '0') {
                        s += 5;
                        *d++ = '\n';
                        continue;
                    } else if (*(s + 3) == '3') {
                        s += 5;
                        // *d++ = '\r';
                        continue;
                    }
                } else if (*(s + 2) == '9' && *(s + 3) == ';') {
                    s += 4;
                    *d++ = '\t';
                    continue;
                }
            }
            else if(*(s + 2) == 't' && *(s + 3) == ';') {
                if(*(s + 1) == 'l') {
                    s += 4;
                    *d++ = '<';
                    continue;
                }
                else if(*(s + 1) == 'g') {
                    s += 4;
                    *d++ = '>';
                    continue;
                }
            }
        }
        *d++ = *s++;
    }
    *d = '\0';
    buffer->len += d - started;
}

bool buffer_xml_extract_and_print_value(BUFFER *buffer, const char *xml, size_t xml_len, const char *prefix, const char *keys[]) {
    return buffer_extract_and_print_xml_with_cb(
        buffer, xml, xml_len,
        prefix, keys,
        print_value_cb);
}
