/*
 * This file is part of the ufo utility.
 * Copyright (C) 2025-2026 Eugene 'Vindex' Stulin
 * Distributed under the BSL 1.0 or the GNU GPL 3.0 or later.
 */

module toml;

import std.algorithm : strip;
static import std.ascii;
import std.conv : to;
import std.range : empty;
import std.string :
    endsWith, join, replace, split, splitLines, startsWith, strip, stripLeft;
import std.format : format, formattedRead;
import std.meta : AliasSeq;
import std.typecons : Tuple, tuple;
import std.regex : regex, matchFirst;
import std.exception : enforce;
import std.datetime : Date, TimeOfDay, SysTime;
static import std.traits;

import amalthea.libcore;
static import amalthea.dataprocessing;
import amalthea.dataprocessing :
    getIndex, getIndexOfLast, isAmong, OrderedMap;

enum TomlNodeType {
    str,
    boolean,
    integer,
    floating,
    systime,
    time,
    date,
    arr,
    table,
    none
}


alias TomlTable = OrderedMap!(string, TomlNode);


struct TomlNode {

public:
    this(string value) {
        store.str = value;
        tvt = TomlNodeType.str;
    }
    this(bool value) {
        store.boolean = value;
        tvt = TomlNodeType.boolean;
    }
    this(long value) {
        store.integer = value;
        tvt = TomlNodeType.integer;
    }
    this(double value) {
        store.floating = value;
        tvt = TomlNodeType.floating;
    }
    this(SysTime value) {
        store.stime = value;
        tvt = TomlNodeType.systime;
    }
    this(TimeOfDay value) {
        store.timeOfDay = value;
        tvt = TomlNodeType.time;
    }
    this(Date value) {
        store.date = value;
        tvt = TomlNodeType.date;
    }
    this(TomlNode[] arr) {
        store.arr = arr;
        tvt = TomlNodeType.arr;
    }
    this(TomlTable value) {
        store.table = value;
        tvt = TomlNodeType.table;
    }

    TomlNodeType getType() const {
        return tvt;
    }

    inout(T) getValue(T)() inout {
        static if (is(T == string)) {
            return store.str;
        }
        static if (is(T == long)) {
            return store.integer;
        }
        static if (is(T == double)) {
            return store.floating;
        }
        static if (is(T == bool)) {
            return store.boolean;
        }
        static if (is(T == SysTime)) {
            return store.stime;
        }
        static if (is(T == TimeOfDay)) {
            return store.timeOfDay;
        }
        static if (is(T == Date)) {
            return store.date;
        }
        static if (is(T == TomlNode[])) {
            return store.arr;
        }
        static if (is(T == TomlTable)) {
            return store.table;
        }
    }

    ref T getRefValue(T)() {
        static if (is(T == string)) {
            return store.str;
        }
        static if (is(T == long)) {
            return store.integer;
        }
        static if (is(T == double)) {
            return store.floating;
        }
        static if (is(T == bool)) {
            return store.boolean;
        }
        static if (is(T == SysTime)) {
            return store.stime;
        }
        static if (is(T == TimeOfDay)) {
            return store.timeOfDay;
        }
        static if (is(T == Date)) {
            return store.date;
        }
        static if (is(T == TomlNode[])) {
            return store.arr;
        }
        static if (is(T == TomlTable)) {
            return store.table;
        }
    }

    ref inout(TomlNode) opIndex(string key) inout {
        auto t = getValue!TomlTable();
        return t[key];
    }

    string getStringValue() const {
        return getValue!string();
    }

    long getIntegerValue() const {
        return getValue!long();
    }

    double getFloatingValue() const {
        return getValue!double();
    }

    bool getBooleanValue() const {
        return getValue!bool();
    }

    SysTime getTimeValue() const {
        return getValue!SysTime();
    }

    TimeOfDay getTimeOfDayValue() const {
        return getValue!TimeOfDay();
    }

    Date getDateValue() const {
        return getValue!Date();
    }

    TomlNode[] getArray() const {
        return amalthea.dataprocessing.copyArray(getValue!(TomlNode[])());
    }

    TomlTable getTable() const {
        return getValue!TomlTable().dup;
    }

    private string getStringFromSimpleType() const
    in {
        alias TNT = TomlNodeType;
        assert(!tvt.isAmong(TNT.arr, TNT.table, TNT.none));
    }
    do {
        final switch (tvt) {
            case TomlNodeType.str:
                return getValue!string();
            case TomlNodeType.boolean:
                return getValue!bool().to!string;
            case TomlNodeType.integer:
                return getValue!long().to!string;
            case TomlNodeType.floating:
                return getValue!double().to!string;
            case TomlNodeType.systime:
                return getValue!SysTime().toISOExtString;
            case TomlNodeType.time:
                return getValue!TimeOfDay().toISOExtString;
            case TomlNodeType.date:
                return getValue!Date().toISOExtString;
            case TomlNodeType.arr:
                return "";  // unattainable
            case TomlNodeType.table:
                return "";  // unattainable
            case TomlNodeType.none:
                return "";  // unattainable
        }
    }

    bool isArrayOfTables() const {
        if (tvt != TomlNodeType.arr) {
            return false;
        }
        const TomlNode[] arr = getValue!(TomlNode[])();
        if (arr.length > 0 && arr[0].tvt == TomlNodeType.table) {
            return true;
        }
        return false;
    }

    bool isTable() const {
        return tvt == TomlNodeType.table;
    }

    bool isArray() const {
        return tvt == TomlNodeType.arr;
    }

    bool isString() const {
        return tvt == TomlNodeType.str;
    }

    string toText(string acckey = "") const {
        string result;
        if (this.isArray) {
            auto arr = getValue!(TomlNode[])();
            if (this.isArrayOfTables) {
                foreach(i, TomlNode elem; arr) {
                    result ~= format!"\n[[%s]]\n%s"(acckey, elem.toText());
                }
            } else {
                result = "[";
                foreach(i, TomlNode elem; arr) {
                    string value = elem.toText();
                    if (elem.tvt == TomlNodeType.str) {
                        value = value.replace(`"`, `\"`).replace(`'`, `\'`);
                        value = `"` ~ value ~ `"`;
                    }
                    result ~= value;
                    if (i+1 != arr.length) {
                        result ~= ", ";
                    }
                }
                result ~= "]";
            }
        } else if (this.isTable) {
            auto table = getValue!TomlTable();
            bool keyPrinted = false;
            foreach(i, string k; table.keys) {
                auto v = table[k];
                if (v.isArrayOfTables()) {
                    result ~= v.toText(k);
                    continue;
                }
                if (!acckey.empty && !keyPrinted) {
                    bool keyContainsDots = (getIndex(k, '.') != -1);
                    k = keyContainsDots ? format!`"%s"`(k) : k;
                    result ~= format!"\n[%s]\n"(acckey.strip('.'));
                    keyPrinted = true;
                }
                string extracted;
                if (v.isArray) {
                    extracted = v.toText();
                    enum fmt = "%s%s = %s\n";
                    result ~= format(fmt, "", k, extracted);
                } else if (v.isTable) {
                    auto tmp = acckey ~ ( k.empty ? k : k ~ "." );
                    result ~= v.toText(tmp);
                    if (i + 1 < table.length) {
                        if (!table[table.keys[i+1]].isTable) {
                            keyPrinted = false;
                        }
                    }
                } else {
                    extracted = v.getStringFromSimpleType();
                    enum strFmt = "%s%s = \"%s\"\n";
                    enum othFmt = "%s%s = %s\n";
                    string fmt = v.isString ? strFmt : othFmt;
                    result ~= format(fmt, "", k, extracted);
                }
            }
        } else {
            result = this.getStringFromSimpleType();
        }
        return result;
    }

private:
    union Store {
        string str;
        bool boolean;
        long integer;
        double floating;
        SysTime stime;
        TimeOfDay timeOfDay;
        Date date;
        TomlNode[] arr;
        TomlTable table;
    }
    Store store;
    TomlNodeType tvt = TomlNodeType.none;
}


TomlNode parseToml(string content) {
    struct TableElement {
        string tableName;
        string key;
        string value;
    }
    TableElement[] tableElements;
    string currentTableName;
    TableElement[][][string] rawArraysOfTables;
    bool inArrayOfTables = false;
    string currentArrayOfTables = "";
    content = handleMultiStrings(content);
    foreach(i, line; content.splitLines) {
        line = removeComment(line);
        line = line.strip;
        if (line.empty) {
            continue;
        }
        if (line.startsWith("[[") && line.endsWith("]]")) {  // array of tables
            inArrayOfTables = true;
            currentArrayOfTables = line[2 .. $-2];
            if (currentArrayOfTables !in rawArraysOfTables) {
                rawArraysOfTables[currentArrayOfTables] = null;
            }
            currentTableName = "";
            rawArraysOfTables[currentArrayOfTables].length++;
        } else if (line.startsWith("[") && line.endsWith("]")) {  // table
            inArrayOfTables = false;
            string tablePath = line[1 .. $-1].strip;
            auto parts = tablePath.split(".");
            foreach(ref part; parts) {
                part = part.strip;
            }
            currentTableName = parts.join(".").idup;
        } else {
            line = line.strip;
            string[] keyAndValue = splitByFirst(line, '=');
            if (keyAndValue.length != 2) {
                throw new TomlException(format!"Incorrect line #%s."(i + 1));
            }
            string k = keyAndValue[0].strip.stripQuotes;
            string tableName;
            AliasSeq!(tableName, k) = sepTableAndKey(k);

            if (tableName.empty && !currentTableName.empty) {
                tableName = currentTableName;
            } else if (!currentTableName.empty) {
                tableName = currentTableName ~ "." ~ tableName;
            }
            string v = keyAndValue[1].strip;
            if (!inArrayOfTables) {
                tableElements ~= TableElement(tableName, k, v);
            } else {
                rawArraysOfTables[currentArrayOfTables][$-1]
                    ~= TableElement(tableName, k, v);
            }
        }
    }
    TomlTable result;
    foreach(elem; tableElements) {
        TomlTable* subresult = &result;
        if (elem.tableName.empty) {
            result[elem.key] = makeTomlNode(elem.value);
        } else {
            auto parts = elem.tableName.split(".");
            foreach(part; parts) {
                if (part !in *subresult) {
                    (*subresult)[part] = TomlNode(TomlTable.init);
                }
                auto tv = part in *subresult;
                subresult = (() => &((*tv).getRefValue!TomlTable()))();
            }
        }
        (*subresult)[elem.key] = makeTomlNode(elem.value);
    }
    // array of tables
    TomlTable[][string] arraysOfTables;
    foreach(k, t; rawArraysOfTables) {
        TomlTable[] cell;
        foreach(TableElement[] tableInArrayCell; t) {
            TomlTable tt;
            foreach(TableElement field; tableInArrayCell) {
                tt[field.key] = makeTomlNode(field.value);
            }
            cell ~= tt;
        }
        arraysOfTables[k] = cell;
    }
    // append arraysOfTables to result
    foreach(string k, TomlTable[] tt; arraysOfTables) {
        if (k in result) {
            throw new TomlException(format!"Re-used name: %s"(k));
        }
        TomlNode[] tmp;
        foreach(e; tt) {
            tmp ~= TomlNode(e);
        }
        result[k] = TomlNode(tmp);
    }
    return TomlNode(result);
}


TomlNode makeTomlNode(string s) {
    // array
    if (s.startsWith(`[`) && s.endsWith(`]`) && !isEscaped(s, s.length - 1)) {
        return TomlNode(makeTomlArray(s));
    }
    // str
    if (s.startsWith(`"`) && s.endsWith(`"`)) {
        return TomlNode(s.stripQuotes);
    }
    // date/time
    try return TomlNode(SysTime.fromISOExtString(s));   catch(Exception e) {}
    try return TomlNode(TimeOfDay.fromISOExtString(s)); catch(Exception e) {}
    try return TomlNode(Date.fromISOExtString(s));      catch(Exception e) {}
    // number
    auto value = parseNumber(s);
    if (value.getType() != TomlNodeType.none) {
        return value;
    }
    // boolean
    if (s == "true") {
        return TomlNode(true);
    } else if (s == "false") {
        return TomlNode(true);
    }
    throw new TomlException(format!"Unrecognized value: %s"(s));
}



class TomlException : Exception { mixin RealizeException; }


private bool isEscaped(string line, size_t index) {
    size_t n = 0;
    for (; index > 0 && line[--index] == '\\'; n++) {}
    return (n % 2) == 1;
}


private string stripQuotes(string s) {
    if (s.length < 2) {
        return s;
    }
    if (s[0] == '"' && s[$-1] == '"' || s[0] == '\'' && s[$-1] == '\'') {
        if (!isEscaped(s, s.length - 1)) {
            s = s[1 .. $-1];
        }
    }
    return s;
}


private string removeComment(string line) {
    size_t index = line.length;
    bool inDoubleQuotes = false;
    bool inOrdinaryQuotes = false;
    foreach(i, ch; line) {
        if (ch == '"' && !isEscaped(line, i)) {
            if (!inOrdinaryQuotes) {
                inDoubleQuotes = !inDoubleQuotes;
            }
        } else if (ch == '\'' && !isEscaped(line, i)) {
            if (!inDoubleQuotes) {
                inOrdinaryQuotes = !inOrdinaryQuotes;
            }
        } else if (ch == '#' && !inOrdinaryQuotes && !inDoubleQuotes) {
            index = i;
            break;
        }
    }
    return line[0 .. index];
}


private Tuple!(string, string) sepTableAndKey(string k) {
    ssize_t[] dotIndexes = [];
    bool inDoubleQuotes = false;
    bool inOrdinaryQuotes = false;
    foreach(j, ch; k) {
        if (ch == '"') {
            if (!inOrdinaryQuotes) {
                inDoubleQuotes = !inDoubleQuotes;
            }
        } else if (ch == '\'') {
            if (!inDoubleQuotes) {
                inOrdinaryQuotes = !inOrdinaryQuotes;
            }
        } else if (ch == '.') {
            if (!inDoubleQuotes && !inOrdinaryQuotes) {
                dotIndexes ~= j;
            }
        }
    }
    string tableName;
    if (!dotIndexes.empty) {
        auto lastIndex = dotIndexes[$-1];
        tableName = k[0 .. lastIndex];
        tableName = tableName.stripQuotes;
        k = k[lastIndex+1 .. $];
        k = k.stripQuotes;
    }
    return tuple(tableName, k);
}


private dstring trimBackSlashWithWhiteSpaces(dstring text) {
    dstring[] splitted = text.split("\\\n");
    if (splitted.length == 1) {
        return text;
    }
    foreach(ref elem; splitted) {
        elem = elem.stripLeft();
    }
    return splitted.join();
}
unittest {
    dstring text = `
The quick brown \


  fox jumps over \
    the lazy dog.`;
    dstring expected = `The quick brown fox jumps over the lazy dog.`;
    assert(trimBackSlashWithWhiteSpaces(text) == expected);
}


private string handleMultiStrings(string text) {
    bool inMultiString = false;
    auto utf32Text = text.to!dstring ~ " ";
    dstring utf32Result;
    ssize_t startPos = -1;
    ssize_t finishPos = -1;
    foreach(i, dchar c; utf32Text) {
        if (!inMultiString && i > 2 && (finishPos == -1 || i > finishPos + 6)) {
            inMultiString = (utf32Text[i - 3 .. i] == `"""`);
            if (inMultiString) {
                startPos = i;
                utf32Result = utf32Result[0 .. $ - 2];  // """ -> "
                continue;
            }
        } else if (inMultiString) {
            if (i > startPos + 3) {
                inMultiString = !(utf32Text[i - 3 .. i] == `"""`);
                if (!inMultiString) {
                    finishPos = i - 3;
                    dstring multistring = utf32Text[startPos .. finishPos];
                    utf32Result ~= trimBackSlashWithWhiteSpaces(multistring);
                    utf32Result ~= ['"', c];
                    startPos = -1;
                }
            }
            continue;
        }
        utf32Result ~= c;
    }
    return utf32Result[0 .. $ - 1].to!string;  // without final space
}
unittest {
    string value = `str1 = "The quick brown fox jumps over the lazy dog."

str2 = """
The quick brown \


  fox jumps over \
    the lazy dog."""

str3 = """\
       The quick brown \
       fox jumps over \
       the lazy dog.\
       """`;

    string expected =
`str1 = "The quick brown fox jumps over the lazy dog."

str2 = "The quick brown fox jumps over the lazy dog."

str3 = "The quick brown fox jumps over the lazy dog."`;

    assert(handleMultiStrings(value) == expected);
}



private TomlNode[] makeTomlArray(string s)
in {
    assert(s.startsWith(`[`) && s.endsWith(`]`) && !isEscaped(s, s.length-1));
}
do {
    TomlNode[] arr;
    s = s[1 .. $-1].strip;
    ssize_t valPos = -1;
    bool inDoubleQuotes = false;
    bool inOrdQuotes = false;
    bool inValue = false;
    bool inArray = false;
    foreach(i, ch; s) {
        bool inQuotes = inDoubleQuotes || inOrdQuotes;
        if (inArray && (ch != ']' || isEscaped(s, i))) {  // in array
            continue;
        } else if (ch == '[' && !isEscaped(s, i)) {
            if (inQuotes) {
                continue;
            }
            inArray = true;
            inValue = true;
            valPos = i;
        } else if (ch == ']' && !isEscaped(s, i)) {
            if (inQuotes) {
                continue;
            } else if (!inArray) {
                throw new TomlException(format!"Unrecognized value: %s"(s));
            }
            inArray = false;
            inValue = false;
            arr ~= TomlNode(makeTomlArray(s[valPos .. i+1]));
            valPos = -1;
        } else if (std.ascii.isWhite(ch)) {
            continue;
        } else if (ch == ',') {
            if (inQuotes) {
                continue;
            }
            if (inValue) {
                inValue = false;
                arr ~= makeTomlNode(s[valPos .. i]);
                valPos = -1;
            }
        } else if ((ch == '"' || ch == '\'') && !isEscaped(s, i)) {
            bool* inCurrQuotes = (ch == '"') ? &inDoubleQuotes : &inOrdQuotes;
            bool* inOtherQuotes = (ch == '"') ? &inOrdQuotes : &inDoubleQuotes;
            if (*inCurrQuotes) {  // close quotes
                arr ~= makeTomlNode(s[valPos .. i + 1]);
                valPos = -1;
            } else if (*inOtherQuotes) {
                continue;
            } else {  // open quotes
                valPos = i;
            }
            inValue = !inValue;
            *inCurrQuotes = !(*inCurrQuotes);
        } else if (i + 1 == s.length) {  // end
            if (!inValue) {
                valPos = i;
            }
            arr ~= makeTomlNode(s[valPos .. $]);
        } else if (!inValue) {
            valPos = i;
            inValue = true;
        }
    }
    return arr;
}


private TomlNode parseNumber(string s) {
    s = s.replace("_", "");
    enum decEx = regex(`^[+-]?[1-9]+[0-9_]*$`);
    enum hexEx = regex(`^[+-]?(0x|0X)[0-9a-fA-F]+$`);
    enum fltEx = regex(`^[+-]?((_?\d+)?\.(_?\d)*|(_?\d)+)([eE][+-]?(_?\d+))?$`);

    const err = format!"Unrecognized value: %s"(s);

    // integer
    if (!matchFirst(s, decEx).empty) {
        s = s.replace("_", "");
        long v;
        enforce!TomlException(1 == formattedRead(s, "%d", v), err);
        return TomlNode(v);
    }
    // hexadecimal
    if (!matchFirst(s, hexEx).empty) {
        long v;
        enforce!TomlException(1 == formattedRead(s[2 .. $], "%x", v), err);
        return TomlNode(v);
    }
    // floating
    if (s == "inf" || s == "+inf") {
        return TomlNode(double.infinity);
    } else if (s == "-inf") {
        return TomlNode(-double.infinity);
    } else if (s == "nan" || s == "+nan") {
        return TomlNode(double.nan);
    } else if (s == "-nan") {
        return TomlNode(-double.nan);
    } else if (!matchFirst(s, fltEx).empty) {
        s = s.replace("_", "");
        double v;
        enforce!TomlException(1 == formattedRead(s, "%f", v), err);
        return TomlNode(v);
    }
    return TomlNode.init;
}


private string[] splitByFirst(string line, char symbol) {
    auto i = getIndex(line, symbol);
    if (i == -1) {
        return [line];
    } else if (i == 0) {
        return ["", line[1 .. $]];
    } else if (i + 1 == line.length) {
        return [line[0 .. $ - 1], ""];
    }
    return [line[0 .. i], line[i + 1 .. $]];
}
unittest {
    assert(["L1", "L2"] == splitByFirst("L1.L2", '.'));
    assert(["L1", "L2.L3"] == splitByFirst("L1.L2.L3", '.'));
    assert(splitByFirst("", '/') == [""]);
    assert(splitByFirst("L1/L2/L3", '.') == ["L1/L2/L3"]);
}


private string[] splitByLast(string line, char symbol) {
    auto i = getIndexOfLast(line, symbol);
    if (i == -1) {
        return [line];
    }
    if (i == 0) {
        return ["", line[1 .. $]];
    } else if (i + 1 == line.length) {
        return [line[0 .. $ - 1], ""];
    }
    return [line[0 .. i], line[i + 1 .. $]];
}
unittest {
    assert(["L1", "L2"] == splitByLast("L1.L2", '.'));
    assert(["L1.L2", "L3"] == splitByLast("L1.L2.L3", '.'));
    assert(splitByLast("", '/') == [""]);
    assert(splitByLast("L1/L2/L3", '.') == ["L1/L2/L3"]);
}
