/* Copyright 2022 the SumatraPDF project authors (see AUTHORS file).
   License: Simplified BSD (see COPYING.BSD) */

#include "BaseUtil.h"
#include "SquareTreeParser.h"

/*
A 'square tree' is a format for representing string values contained in a
tree structure. Consumers may parse these string values further (as integers,
etc.) as required.

Each non-empty line which doesn't start with either '#' or ';' (comment line)
contains either a key and a single-line string value separated by '=' or ':' or
it contains a key followed by a single '[' which causes the following lines to be
parsed as a nested child node until a line starts with ']':

key = value
key2 = [ value containing square brackets (length: 49) ]
child [
  depth = 1
  subchild [
    depth = 2
  ]
]

If tree nodes are either concatenated or if their names are reused for a different
node, these nodes form a list which can be accessed by index (cf. SquareTreeNode::GetChild).

list [
  value 1
] [
  value 2
]
list [
  value 3
]

The below parser always tries to recover from errors, usually by ignoring
faulty lines. One intentional error hanling is the parsing of INI-style headers which
allows to parse INI files mostly as expected as well. E.g.

[Section]
key = value

is read as if it were written as

Section [
  key = value
]

Final note: Whitespace at the start and end of a line as well as around key-value
separators is always ignored.
*/

static inline char* SkipWs(char* s, bool stopAtLineEnd = false) {
    for (; str::IsWs(*s) && (!stopAtLineEnd || *s != '\n'); s++) {
        ;
    }
    return s;
}
static inline char* SkipWsRev(char* begin, char* s) {
    for (; s > begin && str::IsWs(*(s - 1)); s--) {
        ;
    }
    return s;
}

static char* SkipWsAndComments(char* s) {
    do {
        s = SkipWs(s);
        if ('#' == *s || ';' == *s) {
            // skip entire comment line
            for (; *s && *s != '\n'; s++) {
                ;
            }
        }
    } while (str::IsWs(*s));
    return s;
}

static bool IsBracketLine(char* s) {
    if (*s != '[') {
        return false;
    }
    // the line may only contain whitespace and a comment
    for (s++; *s && *s != '\n' && *s != '#' && *s != ';'; s++) {
        if (!str::IsWs(*s)) {
            return false;
        }
    }
    return true;
}

SquareTreeNode::~SquareTreeNode() {
    for (size_t i = 0; i < data.size(); i++) {
        DataItem& item = data.at(i);
        delete item.child;
    }
}

const char* SquareTreeNode::GetValue(const char* key, size_t* startIdx) const {
    int start = startIdx ? (int)*startIdx : 0;
    int n = data.Size();
    for (int i = start; i < n; i++) {
        DataItem& item = data.At(i);
        if (str::EqI(key, item.key) && !item.child) {
            if (startIdx) {
                *startIdx = (size_t)(i + 1);
            }
            return item.str;
        }
    }
    return nullptr;
}

SquareTreeNode* SquareTreeNode::GetChild(const char* key, size_t* startIdx) const {
    int start = startIdx ? (int)*startIdx : 0;
    int n = data.Size();
    for (int i = start; i < n; i++) {
        DataItem& item = data.At(i);
        if (str::EqI(key, item.key) && item.child) {
            if (startIdx) {
                *startIdx = (size_t)(i + 1);
            }
            return item.child;
        }
    }
    return nullptr;
}

static SquareTreeNode* ParseSquareTreeRec(char*& data, bool isTopLevel = false) {
    SquareTreeNode* node = new SquareTreeNode();

    while (*(data = SkipWsAndComments(data))) {
        // all non-empty non-comment lines contain a key-value pair
        // where the value is either a string (separated by '=' or ':')
        // or a list of child nodes (if the key is followed by '[' alone)
        char* key = data;
        for (data = key; *data && *data != '=' && *data != ':' && *data != '[' && *data != ']' && *data != '\n';
             data++) {
            ;
        }
        if (!*data || '\n' == *data) {
            // use first whitespace as a fallback separator
            for (data = key; *data && !str::IsWs(*data); data++) {
                ;
            }
        }
        char* separator = data;
        if (*data && *data != '\n') {
            // skip to the first non-whitespace character on the same line (value)
            data = SkipWs(data + 1, true);
        }
        char* value = data;
        // skip to the end of the line
        for (; *data && *data != '\n'; data++) {
            ;
        }
        if (IsBracketLine(separator) ||
            // also tolerate "key \n [ \n ... \n ]" (else the key
            // gets an empty value and the child node an empty key)
            str::IsWs(*separator) && '\n' == *value && IsBracketLine(SkipWsAndComments(data))) {
            // parse child node(s)
            data = SkipWsAndComments(separator) + 1;
            *SkipWsRev(key, separator) = '\0';
            node->data.Append(SquareTreeNode::DataItem(key, ParseSquareTreeRec(data)));
            // arrays are created by either reusing the same key for a different child
            // or by concatenating multiple children ("[ \n ] [ \n ] [ \n ]")
            while (IsBracketLine((data = SkipWsAndComments(data)))) {
                data++;
                node->data.Append(SquareTreeNode::DataItem(key, ParseSquareTreeRec(data)));
            }
        } else if (']' == *key) {
            // finish parsing child node
            data = key + 1;
            if (!isTopLevel) {
                return node;
            }
            // ignore superfluous closing square brackets instead of
            // ignoring all content following them
        } else if ('[' == *key && ']' == SkipWsRev(value, data)[-1]) {
            // treat INI section headers as top-level node names
            // (else "[Section]" would be ignored)
            if (!isTopLevel) {
                data = key;
                return node;
            }
            // trim whitespace around section name (for consistency with GetPrivateProfileString)
            key = SkipWs(key + 1);
            *SkipWsRev(key, SkipWsRev(value, data) - 1) = '\0';
            node->data.Append(SquareTreeNode::DataItem(key, ParseSquareTreeRec(data)));
        } else if ('[' == *separator || ']' == *separator) {
            // invalid line (ignored)
        } else {
            // string value (decoding is left to the consumer)
            bool hasMoreLines = '\n' == *data;
            *SkipWsRev(key, separator) = '\0';
            *SkipWsRev(value, data) = '\0';
            node->data.Append(SquareTreeNode::DataItem(key, value));
            if (hasMoreLines) {
                data++;
            }
        }
    }

    // assume that all square brackets have been properly balanced
    return node;
}

static void SerializeRec(SquareTreeNode* node, StrBuilder& s, int indent) {
    int n = node->data.Size();
    for (int i = 0; i < n; i++) {
        SquareTreeNode::DataItem& item = node->data.At(i);
        for (int j = 0; j < indent; j++) {
            s.AppendChar(' ');
            s.AppendChar(' ');
        }
        if (item.child) {
            s.Append(item.key);
            s.Append(" [\n");
            SerializeRec(item.child, s, indent + 1);
            for (int j = 0; j < indent; j++) {
                s.AppendChar(' ');
                s.AppendChar(' ');
            }
            s.Append("]\n");
        } else {
            s.Append(item.key);
            s.Append(" = ");
            if (item.str) {
                s.Append(item.str);
            }
            s.AppendChar('\n');
        }
    }
}

const char* SerializeSquareTreeNode(SquareTreeNode* node) {
    if (!node) {
        return nullptr;
    }
    StrBuilder s;
    SerializeRec(node, s, 0);
    return s.StealData();
}

SquareTreeNode* ParseSquareTree(const char* s) {
    TempStr data = strconv::UnknownToUtf8Temp(s);
    if (!data) {
        return nullptr;
    }
    char* tmp = data;
    auto res = ParseSquareTreeRec(tmp, true);
    return res;
}
