// Copyright (c) 2022 Matt Borgerson
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#pragma once

#include <stdio.h>
#include <limits.h>
#include <float.h>
#include <string.h>
#include <assert.h>

#include <algorithm>
#include <memory>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <utility>
#include <vector>

// 3rdparty
#include <toml++/toml.h>

#ifndef DEBUG
#define DEBUG 0
#endif

template< class T >
std::unique_ptr<T> copy_unique(const std::unique_ptr<T>& source)
{
    return source ? std::make_unique<T>(*source) : nullptr;
}

template<typename ... Args>
std::string string_format( const std::string& format, Args ... args )
{
    int size_s = std::snprintf( nullptr, 0, format.c_str(), args ... ) + 1; // Extra space for '\0'
    if( size_s <= 0 ){ throw std::runtime_error( "Error during formatting." ); }
    auto size = static_cast<size_t>( size_s );
    std::unique_ptr<char[]> buf( new char[ size ] );
    std::snprintf( buf.get(), size, format.c_str(), args ... );
    return std::string( buf.get(), buf.get() + size - 1 ); // We don't want the '\0' inside
}

typedef enum CNodeType {
    Array, Boolean, Enum, Integer, Number, String, Table,
} CNodeType;

char const * const type_names[] = {
    "Array", "Boolean", "Enum", "Integer", "Number", "String", "Table",
};

class CNode {
public:
    CNodeType type;
    std::string name;
    std::vector<CNode> children;
    union {
        struct { bool val, default_val; } boolean;
        struct { float min, max, val, default_val; } number;
        struct { int min, max, val, default_val; } integer;
    } data;
    struct { std::string val, default_val; } string;
    std::unique_ptr<CNode> array_item_type;
    struct { std::vector<std::string> values; int val, default_val; } data_enum;

    struct {
        size_t offset, count_offset, size;
    } serialized;

    CNode(const CNode& other)
    {
        type = other.type;
        name = std::string(other.name);
        children = std::vector<CNode>(other.children);
        array_item_type = copy_unique(other.array_item_type);
        data = other.data;
        data_enum = other.data_enum;
        string = other.string;
        serialized = other.serialized;
    }

    CNode& operator=(const CNode& other)
    {
        type = other.type;
        name = std::string(other.name);
        children = std::vector<CNode>(other.children);
        array_item_type = copy_unique(other.array_item_type);
        data = other.data;
        data_enum = other.data_enum;
        string = other.string;
        serialized = other.serialized;

        return *this;
    }

    CNode(std::string name, std::vector<CNode> children = {})
    : type(CNodeType::Table), name(name), children(children) {}

    CNode(std::string name, CNodeType type)
    : type(type), name(name) {}

    //
    // Get child node by name
    //
    CNode *child(std::string_view needle) {
        for (auto &c : children) {
            if (!needle.compare(c.name)) {
                return &c;
            }
        }
        return NULL;
    }

    //
    // Map the enumerated type `value` to the corresponding integer
    //
    int enum_str_to_int(std::string value)
    {
        auto &v = data_enum.values;
        auto it = std::find(v.begin(), v.end(), value);
        return (it != v.end()) ? it - v.begin() : -1;
    }

    //
    // Print indentation to stdout
    //
    void indent(int c) {
        while (c--) printf("  ");
    }

    //
    // Print a representation of the tree to stdout
    //
    void repr(int depth = 0) {
        indent(depth); printf("%s<%s> ", name.c_str(), type_names[type]);

        if (type == Table) {
            printf("\n");
            for (auto &c : children) {
                c.repr(depth + 1);
            }
            return;
        }

        printf("@%zd ", serialized.offset);

        const char *tf_str[2] = { "false", "true" };
        switch (type) {
            case Array:
                printf("%zdB {\n", serialized.size);
                array_item_type->repr(depth+1);
                indent(depth); printf("}\n");
                return;
            case Boolean: printf("%s (d=%s)", tf_str[data.boolean.val], tf_str[data.boolean.default_val]); break;
            case Enum:
                printf("%s (d=%s of { ",
                    data_enum.values[data_enum.val].c_str(),
                    data_enum.values[data_enum.default_val].c_str());
                for (unsigned int i = 0; i < data_enum.values.size(); i++) {
                    if (i) printf(", ");
                    printf("%s ", data_enum.values[i].c_str());
                }
                printf("})");
                break;
            case Integer: printf("%d (d=%d)", data.integer.val, data.integer.default_val); break;
            case Number: printf("%g (d=%g)", data.number.val, data.number.default_val); break;
            case String: printf("\"%s\" (d=\"%s\")", string.val.c_str(), string.default_val.c_str()); break;
            default:
                assert(false);
                break;
        }
        printf("\n");
    }

    //
    // Update tree values from the given TOML table
    //
    void update_from_table(const toml::table &tbl, int depth = 0, std::string path = "")
    {
        for (auto&& [k, v] : tbl) {
            std::string cpath = path.length() ? path + "." + std::string(k)
                                              : std::string(k);

#if DEBUG
            fprintf(stderr, "Currently at %s\n", cpath.c_str());
#endif

            CNode *cnode = child(k.str());
            if (!cnode) {
                fprintf(stderr, "Warning: unrecognized ");
                report_key_line_col(cpath, v.source());
                fprintf(stderr, "\n");
                continue;
            }

            if (!check_type(v, cnode->type)) {
                fprintf(stderr, "Error: incorrect type for ");
                report_key_line_col(cpath, v.source());
                fprintf(stderr, "\n");
                continue;
            }

            if (v.is_table()) {
                cnode->update_from_table(*v.as_table(), depth+1, cpath);
                continue;
            }

            if (cnode->type == Boolean) {
                cnode->set_boolean_tv(*v.value<bool>(), v.source(), cpath);
            } else if (cnode->type == Enum) {
                cnode->set_enum_tv(*v.value<std::string>(), v.source(), cpath);
            } else if (cnode->type == Integer) {
                cnode->set_integer_tv(*v.value<int>(), v.source(), cpath);
            } else if (cnode->type == Number) {
                cnode->set_number_tv(*v.value<float>(), v.source(), cpath);
            } else if (cnode->type == String) {
                cnode->set_string_tv(*v.value<std::string>(), v.source(), cpath);
            } else if (v.is_array()) {
                auto arr = v.as_array();
                int len = arr->size();
                cnode->children.clear();
                if (len > 0) {
                    int i = 0;
                    for (const toml::node &elem : *arr) {
                        if (!check_type(elem, cnode->array_item_type->type)) {
                            fprintf(stderr, "Error: Unexpected array entry type at ");
                            report_line_col(elem.source());
                            fprintf(stderr, "\n");
                            continue;
                        }
                        cnode->children.push_back(*cnode->array_item_type);

                        if (cnode->array_item_type->type == Table) {
                            char buf[8];
                            snprintf(buf, sizeof(buf), "[%d]", i);
                            cnode->children.back().update_from_table(*elem.as_table(), depth+1, cpath + buf);
                            // FIXME: If the item is invalid this leaves default values initialized.
                            //        add an error flag for it
                        } else if (cnode->array_item_type->type == Boolean) {
                            cnode->children.back().set_boolean_tv(*elem.value<bool>(), elem.source(), cpath);
                        } else if (cnode->array_item_type->type == Enum) {
                            cnode->children.back().set_enum_tv(*elem.value<std::string>(), elem.source(), cpath);
                        } else if (cnode->array_item_type->type == Integer) {
                            cnode->children.back().set_integer_tv(*elem.value<int>(), elem.source(), cpath);
                        } else if (cnode->array_item_type->type == Number) {
                            cnode->children.back().set_number_tv(*elem.value<float>(), elem.source(), cpath);
                        } else if (cnode->array_item_type->type == String) {
                            cnode->children.back().set_string_tv(*elem.value<std::string>(), elem.source(), cpath);
                        } else {
                            assert(false);
                        }
                        i++;
                    }
                }
            } else {
                assert(false);
            }
        }
    }

    //
    // Check that the toml node type matches the expected CNode type
    //
    bool check_type(const toml::node &v, CNodeType expected)
    {
        switch (expected) {
        case Array:   return v.is_array();
        case Boolean: return v.is_boolean();
        case Enum:    return v.is_string();
        case Integer: return v.is_integer();
        case Number:  return v.is_number();
        case String:  return v.is_string();
        case Table:   return v.is_table();
        default: assert(false);
        }
    }

    //
    // Output "line y column z" to stderr for given input source region
    //
    void report_line_col(const toml::source_region &src) {
        fprintf(stderr, "line %d column %d", src.begin.line, src.begin.column);
    }

    //
    // Output "key 'x' at line y column z" to stderr for given input source region
    //
    void report_key_line_col(const std::string &key, const toml::source_region &src)
    {
        fprintf(stderr, "key '%s' at ", key.c_str());
        report_line_col(src);
    }

    //
    // Set boolean value
    //
    void set_boolean_tv(bool v, const toml::source_region &from, std::string path)
    {
#if DEBUG
        fprintf(stderr, "%s<%s> = %d at ", path.c_str(), type_names[type], v);
        report_line_col(from);
        fprintf(stderr, "\n");
#endif
        data.boolean.val = v;
    }

    //
    // Set enumerated type value
    //
    void set_enum_by_index(int idx)
    {
        data_enum.val = idx;
    }

    void set_enum_tv(std::string v, const toml::source_region &from, std::string path)
    {
        int idx = enum_str_to_int(v);
        if (idx < 0) {
            fprintf(stderr, "Error: invalid value for ");
            report_key_line_col(path, from);
            fprintf(stderr, "\n");
            return;
        }

#if DEBUG
        fprintf(stderr, "%s<%s> = %s at ", path.c_str(), type_names[type], v.c_str());
        report_line_col(from);
        fprintf(stderr, "\n");
#endif
        set_enum_by_index(idx);
    }

    //
    // Set integer value
    //
    void set_integer(int v)
    {
        data.integer.val = v;
    }

    void set_integer_tv(int v, const toml::source_region &from, std::string path)
    {
#if DEBUG
        fprintf(stderr, "%s<%s> = %d at ", path.c_str(), type_names[type], v);
        report_line_col(from);
        fprintf(stderr, "\n");
#endif
        set_integer(v);
    }

    //
    // Set number value
    //
    void set_number(float v)
    {
        data.number.val = v;
    }

    void set_number_tv(float v, const toml::source_region &from, std::string path)
    {
#if DEBUG
        fprintf(stderr, "%s<%s> = %g at ", path.c_str(), type_names[type], v);
        report_line_col(from);
        fprintf(stderr, "\n");
#endif
        set_number(v);
    }

    //
    // Set string value
    //
    void set_string(std::string v)
    {
        string.val = v;
    }

    void set_string_tv(std::string v, const toml::source_region &from, std::string path)
    {
#if DEBUG
        fprintf(stderr, "%s<%s> = '%s' at ", path.c_str(), type_names[type], v.c_str());
        report_line_col(from);
        fprintf(stderr, "\n");
#endif
        set_string(v);
    }

    //
    // Store values of this node and its children to the structure `s` associated with the tree
    //
    void store_to_struct(void *s)
    {
        uint8_t *p = (uint8_t *)s + serialized.offset;
#if DEBUG
        fprintf(stderr, "Storing %s to offset %d @ %p\n", name.c_str(), serialized.offset, p);
#endif

        switch (type) {
        case Array: {
            int *pc = (int *)((uint8_t *)s + serialized.count_offset);
            *pc = children.size();
            if (children.size()) {
                *(void**)p = calloc(children.size(), serialized.size);
                p = (uint8_t *) *(void**)p;
                for (unsigned int i = 0; i < children.size(); i++) {
                    children[i].store_to_struct(p);
                    p += serialized.size;
                }
            } else {
                *(void**)p = NULL;
            }
            break;
        }
        case Boolean: *(bool*)p  = data.boolean.val;           break;
        case Enum:    *(int*)p   = data_enum.val;              break;
        case Integer: *(int*)p   = data.integer.val;           break;
        case Number:  *(float*)p = data.number.val;            break;
        case String:  *(char**)p = strdup(string.val.c_str()); break;
        case Table:
            for (auto &c : children) {
                c.store_to_struct(s);
            }
            break;
        default: assert(false);
        }
    }

    //
    // Free any allocations made
    //
    void free_allocations(void *s)
    {
        uint8_t *p = (uint8_t *)s + serialized.offset;
#if DEBUG
        fprintf(stderr, "Free %s offset %d @ %p\n", name.c_str(), serialized.offset, p);
#endif

        switch (type) {
        case Array: {
            int *pc = (int *)((uint8_t *)s + serialized.count_offset);
            *pc = 0;
            if (children.size()) {
                uint8_t *p_c = (uint8_t *) *(void**)p;
                for (unsigned int i = 0; i < children.size(); i++) {
                    children[i].free_allocations(p_c);
                    p_c += serialized.size;
                }

                free(*(void**)p);
            }
            *(void**)p = NULL;
            break;
        }
        case String:
            free(*(void**)p);
            *(void**)p = NULL;
            break;
        case Table:
            for (auto &c : children) {
                c.free_allocations(s);
            }
            break;
        default: break;
        }
    }

    //
    // Update values of this node and its children from the structure `s` associated with the tree
    //
    void update_from_struct(void *s)
    {
        uint8_t *p = (uint8_t *)s + serialized.offset;
#if DEBUG
        fprintf(stderr, "Loading %s from offset %d @ %p\n", name.c_str(), serialized.offset, p);
#endif

        switch (type) {
        case Array: {
            int *pc = (int *)((uint8_t *)s + serialized.count_offset);
            children.clear();
            p = (uint8_t *) *(void**)p;
            for (int i = 0; i < *pc; i++) {
                children.push_back(*array_item_type);
                children.back().update_from_struct(p);
                p += serialized.size;
            }
            break;
        }
        case Boolean: data.boolean.val = *(bool*)p;               break;
        case Enum:    data_enum.val    = *(int*)p;                break;
        case Integer: data.integer.val = *(int*)p;                break;
        case Number:  data.number.val  = *(float*)p;              break;
        case String:  string.val       = std::string(*(char**)p); break;
        case Table:
            for (auto &c : children) {
                c.update_from_struct(s);
            }
            break;
        default: assert(false);
        }
    }

    //
    // Check if this node's value differs from its default value
    //
    // Note: Arrays of size > 0 are always considered differing
    //
    bool differs_from_default()
    {
        switch (type) {
        case Array:   return children.size() > 0;
        case Boolean: return data.boolean.val != data.boolean.default_val;
        case Enum:    return data_enum.val    != data_enum.default_val;
        case Integer: return data.integer.val != data.integer.default_val;
        case Number:  return data.number.val  != data.number.default_val;
        case String:  return string.val.compare(string.default_val);
        case Table:
            for (auto &c : children) {
                if (c.differs_from_default()) {
                    return true;
                }
            }
            break;
        default: assert(false);
        }

        return false;
    }

    //
    // Set the current value at every node as the node's default value
    //
    void set_defaults()
    {
        switch (type) {
        case Array:   // XXX: Setting array defaults not supported.
                      //      You'd need to change array_item_type.
                      break;
        case Boolean: data.boolean.default_val = data.boolean.val; break;
        case Enum:    data_enum.default_val = data_enum.val; break;
        case Integer: data.integer.default_val = data.integer.val; break;
        case Number:  data.number.default_val = data.number.val; break;
        case String:  string.default_val = string.val; break;
        case Table:   for (auto &c : children) c.set_defaults(); break;
        default: assert(false);
        }
    }

    //
    // Set the current value at every node to the node's default value
    //
    void reset_to_defaults()
    {
        switch (type) {
        case Array:   // XXX: Setting array defaults not supported.
                      //      You'd need to change array_item_type.
                      break;
        case Boolean: data.boolean.val = data.boolean.default_val; break;
        case Enum:    data_enum.val = data_enum.default_val; break;
        case Integer: data.integer.val = data.integer.default_val; break;
        case Number:  data.number.val = data.number.default_val; break;
        case String:  string.val = string.default_val; break;
        case Table:   for (auto &c : children) c.reset_to_defaults(); break;
        default: assert(false);
        }
    }

    //
    // Generate and return a TOML-formatted configuration for nodes that differ from their default value
    //
    // Note: Arrays of size > 0 are always considered differing
    //
    std::string generate_delta_toml(std::string path = "", bool inline_table = false, int depth = 0, bool root = true)
    {
        if (!differs_from_default()) {
            return "";
        }

        if (type == Table) {
            std::string s = "";
            bool printed_table_header = false;

            std::string cpath;
            if (path.length()) {
                cpath = path + "." + name;
            } else if (!root) {
                cpath = name;
            }

            if (inline_table) {
                if (!name.empty()) {
                    s += name;
                    s += " = ";
                }
                s += "{ ";
            }

            int i = 0;
            for (auto &c : children) {
                if (c.type == Table || !c.differs_from_default()) continue;

                if (!printed_table_header && !inline_table) {
                    if (cpath.length()) {
                        s += "[" + cpath + "]\n";
                    }
                    printed_table_header = true;
                }

                if (inline_table && i++) s += ", ";
                s += c.generate_delta_toml("", inline_table, depth, false);

                if (!inline_table) {
                    s += "\n";
                }
            }

            if (printed_table_header) {
                s += "\n";
            }

            for (auto &c : children) {
                if (c.type != Table || !c.differs_from_default()) continue;
                if (inline_table && i++) s += ", ";
                s += c.generate_delta_toml(cpath, inline_table, depth, false);
            }

            if (inline_table) {
                s += "}";
            }

            return s;
        }

        std::string s = "";

        if (name.length()) {
            s += string_format("%s = ", name.c_str());
        }

        switch (type) {
        case Array: {
            s += "[\n";
            int i = 0;
            for (auto &c : children) {
                if (i++) {
                    s += ",\n";
                }
                for (int d = 0; d < (depth+1); d++) s += "    ";
                s += c.generate_delta_toml("", true, depth + 1, false);
            }
            s += "\n";
            for (int d = 0; d < (depth+1); d++) s += "    ";
            s += "]";
            break;
        }
        case Boolean: s += data.boolean.val ? "true" : "false"; break;
        case Enum: {
            std::ostringstream oss;
            oss << toml::value<std::string>(data_enum.values[data_enum.val]);
            s += oss.str();
            break;
        }
        case Integer: s += string_format("%d", data.integer.val); break;
        case Number:  s += string_format("%g", data.number.val); break;
        case String: {
            std::ostringstream oss;
            oss << toml::value<std::string>(string.val);
            s += oss.str();
            break;
        }
        default: assert(false);
        }

        return s;
    }
};

//
// Helpers to generate CNodes
//

static inline CNode ctab(std::string name, std::vector<CNode> children) {
    return CNode(name, children);
}

static inline CNode carray(size_t o, size_t oc, size_t sz, std::string name, CNode item_type) {
    CNode node(name, Array);
    node.array_item_type = std::make_unique<CNode>(item_type);
    node.serialized.offset = o;
    node.serialized.count_offset = oc;
    node.serialized.size = sz;
    return node;
}

static inline CNode cbool(size_t o, std::string name, bool val = false) {
    CNode node(name, Boolean);
    node.data.boolean = { val, val };
    node.serialized.offset = o;
    return node;
}

static inline CNode cenum(size_t o, std::string name, std::vector<std::string> values, std::string value) {
    CNode node(name, Enum);
    node.data_enum.values = values;
    int idx = node.enum_str_to_int(value);
    assert(idx >= 0 && "Default value invalid");
    node.data_enum.val = node.data_enum.default_val = idx;
    node.serialized.offset = o;
    return node;
}

static inline CNode cinteger(size_t o, std::string name, int val = 0, int min = INT_MIN, int max = INT_MAX) {
    CNode node(name, Integer);
    node.data.integer = { min, max, val, val };
    node.serialized.offset = o;
    return node;
}

static inline CNode cnumber(size_t o, std::string name, float val = 0, float min = -FLT_MAX, float max = +FLT_MAX) {
    CNode node(name, Number);
    node.data.number = { min, max, val, val };
    node.serialized.offset = o;
    return node;
}

static inline CNode cstring(size_t o, std::string name, std::string val) {
    CNode node(name, String);
    node.string = { val, val };
    node.serialized.offset = o;
    return node;
}
