#pragma once
#include "maphoon-lexer/includes.h"
#include "parser_tools.h"
#include "graphchart.h"
#include "gvcsh.h"
#include "gv_lang_misc.h"

namespace graph {

/* The DOT language can have preprocessor directives in the form of
 * #line <num> "file", which act as the preprocessor directives in C.
 * We ignore these for now.*/

/* This class contains the tokenizer grammar for the main file.
 * It cannot generate the full symbol, just the token type. */
template <typename TOKEN>
lexing::classifier<char, TOKEN> build_classifier() {
    using namespace lexing;
    classifier< char, TOKEN > cl(TOKEN::TOK_UNRECOGNIZED_CHAR);

    /* Newline characters in all forms accepted
     * This is a departure from original dot, as the \r is just whitespace */
    const auto newline = just('\n') | just('\r') | just('\r') * just('\n');
    cl.insert(newline, TOKEN::TOK_NEWLINE);
    /* # starts a comment last until end of line */
    auto noCRLF = every<char>().without('\n').without('\r');
    cl.insert(just('#') * noCRLF.star(), TOKEN::TOK_COMMENT);
    /* // starts a comment last until end of line */
    cl.insert(just('/') * just('/') * noCRLF.star(), TOKEN::TOK_COMMENT);
    /* / * .. * / block comments*/
    cl.insert(word("/*") *
              (every<char>().without('*') |
               (just('*').plus() * every<char>().without('/').without('*')).star()
               ).star() * just('*').plus() * just('/'), TOKEN::TOK_COMMENT);
      /* / * ... unclosed block comments */
    cl.insert(word("/*"), TOKEN::TOK_COMMENT);
    /* ignore whitespace */
    auto WS = just(' ') | just('\t');
    cl.insert(WS, TOKEN::TOK_WHITESPACE);
    /* ignore BOM */
    cl.insert(word("\xEF\xBB\xBF"), TOKEN::TOK_WHITESPACE);

    cl.insert(just('='), TOKEN::TOK_EQUAL);
    cl.insert(just(','), TOKEN::TOK_COMMA);
    cl.insert(just(';'), TOKEN::TOK_SEMICOLON);
    cl.insert(just('~'), TOKEN::TOK_TILDE);
    cl.insert(just('+'), TOKEN::TOK_TILDE);
    cl.insert(just(':'), TOKEN::TOK_COLON);
    cl.insert(just('('), TOKEN::TOK_OPARENTHESIS);
    cl.insert(just(')'), TOKEN::TOK_CPARENTHESIS);
    cl.insert(just('['), TOKEN::TOK_OSBRACKET);
    cl.insert(just(']'), TOKEN::TOK_CSBRACKET);
    cl.insert(just('{'), TOKEN::TOK_OCBRACKET);
    cl.insert(just('}'), TOKEN::TOK_CCBRACKET);

    cl.insert(word("->"), TOKEN::TOK_EDGEOP);
    cl.insert(word("<->"), TOKEN::TOK_EDGEOP);
    cl.insert(word("<-"), TOKEN::TOK_EDGEOP);
    cl.insert(word("--"), TOKEN::TOK_EDGEOP);
    cl.insert(word("=>"), TOKEN::TOK_EDGEOP);
    cl.insert(word("<=>"), TOKEN::TOK_EDGEOP);
    cl.insert(word("<="), TOKEN::TOK_EDGEOP);
    cl.insert(word("=="), TOKEN::TOK_EDGEOP);
    cl.insert(word(">>"), TOKEN::TOK_EDGEOP);
    cl.insert(word("<<>>"), TOKEN::TOK_EDGEOP);
    cl.insert(word("<<"), TOKEN::TOK_EDGEOP);
    cl.insert(word("++"), TOKEN::TOK_EDGEOP);
    cl.insert(word(">"), TOKEN::TOK_EDGEOP);
    cl.insert(word("<>"), TOKEN::TOK_EDGEOP);
    cl.insert(word("<"), TOKEN::TOK_EDGEOP);
    cl.insert(word(".."), TOKEN::TOK_EDGEOP);
    cl.insert(word("->>"), TOKEN::TOK_EDGEOP);
    cl.insert(word("<<->>"), TOKEN::TOK_EDGEOP);
    cl.insert(word("<<-"), TOKEN::TOK_EDGEOP);
    cl.insert(word("---"), TOKEN::TOK_EDGEOP);
    cl.insert(word("==>"), TOKEN::TOK_EDGEOP);
    cl.insert(word("<==>"), TOKEN::TOK_EDGEOP);
    cl.insert(word("<=="), TOKEN::TOK_EDGEOP);
    cl.insert(word("==="), TOKEN::TOK_EDGEOP);

    /* This is a colon-quoted string, finished by a quotation mark (UTF-8 allowed)
     * :: "<string>"
     * ... or a colon-quoted string, finished by a newline or file-end (UTF-8 allowed)
     * :: "<string>$
     * <string> can contain escaped quotation marks, hashmarks, but no line breaks
     */
    cl.insert(just(':') * just(':') * WS.star() * just('"') *
              (noCRLF.without('"') | (word("\\\""))).star() *
              just('"').optional(), TOKEN::TOK_COLON_QUOTED_STRING);
    /* This is a non quoted colon-string
     * :: <string>
     * terminated by any of: [ { or ;
     * Honors escaping of the above via a backslash. (UTF-8 allowed)
     * Can contain quotation marks (escaped or unescaped), but can not start with it
     * If it contains a hashmark, unescaped [ { or ; is allowed till the end of the line
     * (representing a commented section inside a label)
     */
    auto noterm = every<char>().without(";[{#\\");
    cl.insert(just(':') * just(':') * (WS | newline).star() *
              (
                  just('#') * noCRLF.star() | //starts with a comment
                  just('\\') * noCRLF |       //or an escape sequence
                  noterm.without("\" \t\n\r") //or something else, but not a quotation mark or whitespace
              ) * (
                  just('#') * noCRLF.star() | // continues with a comment
                  just('\\') * noCRLF |       //or an escape sequence
                  noterm                      // or something else
              ).star(),
              TOKEN::TOK_COLON_STRING);
    //degenerate colon string: empty or just a solo escape char
    cl.insert(just(':') * just(':') * WS.star() * just('\\').optional(), TOKEN::TOK_COLON_STRING);

    /* A simple quoted string, that can have escaped quotation marks inside. (UTF-8 allowed)
     * ...or a quoted string missing the terminating quotation mark (at line-end).*/
    cl.insert(just('"') * (noCRLF.without('"') | word("\\\"")).star() * just('"').optional(),
              TOKEN::TOK_QSTRING);

    /* Numbers */
    auto digit = range('0', '9');
    auto dot = just('.');
    auto sign = oneof("+-");
    auto num = digit.plus() * (dot * digit.star()).optional(); //no sign
    cl.insert(sign.optional() * num, TOKEN::TOK_STRING);

    /* Strings (UTF-8 allowed)
     * Starts with letter or underscore, may also include numbers or dots
     * (but not 2 consecutive dots and dots may not be followed by digits).
     * May NOT end in dots as other languages allow.*/
    auto utf8 = range('A', 'Z') | range('a', 'z') | just('_') | range('\x80', '\xff');
    auto str = utf8 * (utf8 | digit | dot * utf8).star();
    cl.insert(str, TOKEN::TOK_STRING);

   /* Parameter names: $ followed by a string not ending with a dot.
    * A single '$" also matches.*/
    cl.insert(just('$') * utf8 * (utf8 | digit | dot * utf8).star(), TOKEN::TOK_PARAM_NAME);
    cl.insert(just('$'), TOKEN::TOK_PARAM_NAME);
    /* Parameter names: $$ representing a value unique to the actual invocation.*/
    cl.insert(just('$') * just('$'), TOKEN::TOK_PARAM_NAME);

    //Have concrete keywords (also matching strings) late as for
    //equal length matches, the later will be selected.

    /* These keywords are case insensitive */
    cl.insert(word("node"), TOKEN::TOK_NODE);
    cl.insert(word("edge"), TOKEN::TOK_EDGE);
    cl.insert(word("graph"), TOKEN::TOK_GRAPH);
    cl.insert(word("digraph"), TOKEN::TOK_DIGRAPH);
    cl.insert(word("strict"), TOKEN::TOK_STRICT);
    cl.insert(word("subgraph"), TOKEN::TOK_SUBGRAPH);
    cl.insert(word("cluster"), TOKEN::TOK_CLUSTER);
    cl.insert(word("defdesign"), TOKEN::TOK_DEFDESIGN);
    cl.insert(word("defstyle"), TOKEN::TOK_DEFSTYLE);
    cl.insert(word("usedesign"), TOKEN::TOK_USEDESIGN);
    cl.insert(word("defproc"), TOKEN::TOK_COMMAND_DEFPROC);
    cl.insert(word("replay"), TOKEN::TOK_COMMAND_REPLAY);
    cl.insert(word("set"), TOKEN::TOK_COMMAND_SET);
    cl.insert(word("include"), TOKEN::TOK_COMMAND_INCLUDE);
    cl.insert(word("if"), TOKEN::TOK_IF);
    cl.insert(word("then"), TOKEN::TOK_THEN);
    cl.insert(word("else"), TOKEN::TOK_ELSE);

    cl = make_deterministic(cl);
    cl = minimize(cl);
    return cl;
}

/* This class contains the tokenizer grammar for html strings.
 * It cannot generate the full symbol, just the token type. */
template <typename TOKEN>
lexing::classifier<char, TOKEN> build_classifier_html_string() {
    using namespace lexing;
    classifier< char, TOKEN > cl(TOKEN::TOK_UNRECOGNIZED_CHAR);

    /* Newline characters in all forms accepted */
    const auto newline = just('\n') | just('\r') | just('\r') * just('\n');
    cl.insert(newline, TOKEN::TOK_NEWLINE);

    cl.insert(just('<'), TOKEN::TOK_EDGEOP); //convention to mean HTML start
    cl.insert(just('>'), TOKEN::TOK_BYE); //convention to mean HTML end

    cl.insert(every<char>().without("\n\r<>"), TOKEN::TOK_STRING);

    cl = make_deterministic(cl);
    cl = minimize(cl);
    return cl;
}

//When we do Color Syntax Highlight parsing the chart object is Csh instead.
template <bool CSH>
using ChartOrCsh = std::conditional<CSH, GraphCsh, GraphChart>::type;

/* This class tokenizes the input and generates a full symbol. */

template <typename TOKEN, bool CSH, typename READER_FN, typename CSH_STYPE, typename CSH_LTYPE>
    requires std::is_enum_v<TOKEN>
TOKEN get_token(sv_reader<CSH>& inp, ChartOrCsh<CSH>& C, READER_FN readandclassify, READER_FN readandclassify_html,
                CSH_STYPE& val, CSH_LTYPE& loc, gv_parser_helper<CSH> &helper) {
    while (true) {
        inp.try_pop();
        if (!inp.has(1)) {
            loc = inp.template commit<false, false>(0);
            return TOKEN::TOK_EOF;
        }
        const auto [int_token, len] = readandclassify(0, inp);
        const TOKEN token = TOKEN(int_token);
        if (len == 0) {
            loc = inp.template commit<false, false>(1);
            return TOKEN::TOK_UNRECOGNIZED_CHAR;
        }
        if (helper.html_may_come) {
            if (inp.peek(0) == '<') {
                int html_nest = 1;
                std::string_view html = inp.view();
                size_t total_len = 1;
                auto start_loc = inp.template commit<false, false>(1).get_begin();
                while (inp.has(1)) {
                    const auto [int_token, len] = readandclassify_html(0, inp);
                    total_len += len;
                    switch (TOKEN(int_token)) {
                    case TOKEN::TOK_NEWLINE:
                        inp.template commit<true, false>(len);
                        continue;
                    case TOKEN::TOK_EDGEOP:
                        html_nest++;
                        inp.template commit<false, false>(len);
                        continue;
                    case TOKEN::TOK_BYE:
                        inp.template commit<false, false>(len);
                        if (!--html_nest) break;
                        continue;
                    case TOKEN::TOK_STRING:
                        inp.template commit<false, false>(len);
                        continue;
                    default:
                        _ASSERT(0);
                        inp.template commit<false, false>(len);
                        break;
                    }
                    break;
                }
                loc.set(start_loc, inp.current_pos());
                val.str.set(html.substr(0, total_len));
                return TOKEN::TOK_QSTRING;
            } else if (token != TOKEN::TOK_WHITESPACE && token != TOKEN::TOK_NEWLINE && token != TOKEN::TOK_COMMENT)
                helper.html_may_come = false;
        }
        switch (token) {
        case TOKEN::TOK_WHITESPACE:
            inp.template commit<false, false>(len);
            continue;
        case TOKEN::TOK_NEWLINE:
            inp.template commit<true, false>(len);
            continue;
        case TOKEN::TOK_COMMENT: {
            //if equals to "/*" then this is a comment, that is not closed(file contains no "*/")
            const bool open_only = inp.view(len) == "/*";
            auto loc = inp.template commit<true, false>(len);
            if constexpr (CSH)
                C.AddCSH(loc, COLOR_COMMENT);
            if (open_only) {
                if constexpr (CSH)
                    C.AddCSH_Error(loc, "Unpaired beginning of block comment '/" "*'.");
                else
                    C.Error.Error(loc.start(), "Unpaired beginning of block comment '/" "*'.");
            }
            continue;
        }
        case TOKEN::TOK_EQUAL:
            helper.html_may_come = true;
            FALLTHROUGH;
        case TOKEN::TOK_COMMA:
        case TOKEN::TOK_SEMICOLON:
        case TOKEN::TOK_TILDE:
        case TOKEN::TOK_COLON:
        case TOKEN::TOK_OPARENTHESIS:
        case TOKEN::TOK_CPARENTHESIS:
        case TOKEN::TOK_OSBRACKET:
        case TOKEN::TOK_CSBRACKET:
        case TOKEN::TOK_UNRECOGNIZED_CHAR:
        case TOKEN::TOK_EOF:
            loc = inp.template commit<false, false>(len);
            return token;

        case TOKEN::TOK_OCBRACKET:
        case TOKEN::TOK_CCBRACKET:
            val.input_text_ptr = inp.view(1).data();
            loc = inp.template commit<false, false>(len);
            return token;

        case TOKEN::TOK_EDGEOP:
            val.edgetype = [text = inp.view(len)]() -> GraphEdgeType {
                switch (text.size()) {
                case 1:
                    _ASSERT(text == ">" || text == "<");
                    if (text.front()=='>') return {GraphEdgeType::DOTTED, GraphEdgeType::FWD};  //>
                                           return {GraphEdgeType::DOTTED, GraphEdgeType::BACK}; //<
                case 2:
                    if (text.front() == '<') {
                        _ASSERT(text == "<-" || text == "<=" || text == "<<" || text == "<>");
                        if (text[1]=='-') return {GraphEdgeType::SOLID, GraphEdgeType::BACK};  //"<-"
                        if (text[1]=='=') return {GraphEdgeType::DOUBLE, GraphEdgeType::BACK}; //"<="
                        if (text[1]=='<') return {GraphEdgeType::DASHED, GraphEdgeType::BACK}; //"<<"
                                          return {GraphEdgeType::DOTTED, GraphEdgeType::BIDIR}; //"<>"

                    } else if (text[1] == '>') {
                        _ASSERT(text == "->" || text == "=>" || text == ">>");
                        if (text.front()=='-') return {GraphEdgeType::SOLID, GraphEdgeType::FWD};  //"->"
                        if (text.front()=='=') return {GraphEdgeType::DOUBLE, GraphEdgeType::FWD}; //"->"
                                               return {GraphEdgeType::DASHED, GraphEdgeType::FWD}; //">>"
                    } else {
                        _ASSERT(text == "--" || text == "==" || text == "++" || text == "..");
                        if (text.front()=='-') return {GraphEdgeType::SOLID, GraphEdgeType::NO_ARROW}; //"--"
                        if (text.front()=='=') return {GraphEdgeType::DOUBLE, GraphEdgeType::NO_ARROW};//"=="
                        if (text.front()=='+') return {GraphEdgeType::DASHED, GraphEdgeType::NO_ARROW};//"++"
                                               return {GraphEdgeType::DOTTED, GraphEdgeType::NO_ARROW};//".."
                    }
                case 3:
                    _ASSERT(text == "<=>" || text == "<->" || text == "->>" || text == "==>" || text == "<<-" || text == "<==" || text == "---" || text == "===");
                    if (text.front() == '<') {
                        if (text[2]=='>') return {text[1]=='=' ? GraphEdgeType::DOUBLE : GraphEdgeType::SOLID, GraphEdgeType::BIDIR}; //"<=>" and "<->"
                                          return {text[2]=='=' ? GraphEdgeType::DOUBLE2: GraphEdgeType::SOLID2,GraphEdgeType::BACK}; //"<<-" and "<=="

                    }
                    if (text[2]=='>') return {text.front()=='=' ? GraphEdgeType::DOUBLE2: GraphEdgeType::SOLID2,GraphEdgeType::FWD}; //"==>" and "->>"
                                      return {text.front()=='=' ? GraphEdgeType::DOUBLE2: GraphEdgeType::SOLID2,GraphEdgeType::NO_ARROW}; //"===" and "---"
                case 4:
                    _ASSERT(text == "<<>>" || text == "<==>");
                    return {text[1]=='<' ? GraphEdgeType::DASHED : GraphEdgeType::DOUBLE2, GraphEdgeType::BIDIR}; //"<<>>" and "<==>"
                case 5:
                    _ASSERT(text == "<<->>");
                    return {GraphEdgeType::SOLID2, GraphEdgeType::BIDIR};
                default:
                    _ASSERT(0);
                    return {};
                }
            } ();
            loc = inp.template commit<false, false>(len);
            return TOKEN::TOK_EDGEOP;

        case TOKEN::TOK_GRAPH:
        case TOKEN::TOK_DIGRAPH:
            val.boolean = token == TOKEN::TOK_DIGRAPH;
            loc = inp.template commit<false, false>(len);
            return token;

        case TOKEN::TOK_NODE:
        case TOKEN::TOK_EDGE:
        case TOKEN::TOK_STRICT:
        case TOKEN::TOK_SUBGRAPH:
        case TOKEN::TOK_CLUSTER:
        case TOKEN::TOK_DEFDESIGN:
        case TOKEN::TOK_DEFSTYLE:
        case TOKEN::TOK_USEDESIGN:
        case TOKEN::TOK_COMMAND_DEFPROC:
        case TOKEN::TOK_COMMAND_REPLAY:
        case TOKEN::TOK_COMMAND_SET:
        case TOKEN::TOK_COMMAND_INCLUDE:
        case TOKEN::TOK_IF:
        case TOKEN::TOK_THEN:
        case TOKEN::TOK_ELSE:
            loc = inp.template commit<false, false>(len);
            return token;

        //special tokens, whose value is their text
        case TOKEN::TOK_PARAM_NAME:
            val.str = inp.view(len);
            loc = inp.template commit<false, false>(len);
            return token;

        case TOKEN::TOK_COLON_STRING: {
            const std::string_view text = inp.view(len);
            _ASSERT(text.starts_with("::"));
            loc = inp.template commit<true, false>(len);
            val.multi_str.init();
            if constexpr (CSH) {
                C.AddCSH_ColonString_CheckAndAddEscapeHint(loc, text, true, true);
                C.AddColonLabel(loc, text);
            } else
                val.multi_str.set_owning(process_colon_string(text, loc.start(), true));
            return TOKEN::TOK_COLON_STRING;
        }
        case TOKEN::TOK_COLON_QUOTED_STRING: { //This is transmuted to a TOK_COLON_STRING
            //if last char is not '"' the quotation is not closed
            std::string_view text = inp.view(len);
            _ASSERT(text.starts_with("::"));
            loc = inp.template commit<false, false>(len);
            if constexpr (CSH) {
                if (!text.ends_with('"'))
                    C.AddCSH_ErrorAfter(loc, "Missing closing quotation mark.");
                C.AddCSH_ColonString_CheckAndAddEscapeHint(loc, text, false, true);
                C.AddColonLabel(loc, text);
                val.multi_str.init();
            } else {
                const char* const colon_pos = text.data();
                const bool closed = text.ends_with('"');
                if (closed)
                    text.remove_suffix(1);
                text.remove_prefix(1); //the colon
                remove_head_tail_whitespace(text);
                FileLineCol pos = loc.start();
                pos.AdvanceCol(text.data() - colon_pos);
                if (!closed)
                    C.Error.Error(loc.start(),
                                  "This opening quotation mark misses its closing pair. "
                                  "Assuming string termination at line-end.",
                                  "Quoted strings cannot have line breaks. Use \'\\n\' to insert a line break.");
                text.remove_prefix(1); //leading quotation mark
                pos.AdvanceCol(1);
                val.multi_str.CombineThemToMe(pos.Print(), text);
            }
            return TOKEN::TOK_COLON_STRING;
        }
        case TOKEN::TOK_STRING: {
            val.str.set(inp.view(len));
            loc = inp.template commit<false, false>(len);
            return TOKEN::TOK_STRING;
        }
        case TOKEN::TOK_QSTRING: {
            std::string_view text = inp.view(len);
            loc = inp.template commit<false, false>(len);
            text.remove_prefix(1); //opening quotation mark
            //TOK_QSTRING: if last char is not '"' the quotation is not closed
            if (text.ends_with('"'))
                text.remove_suffix(1); //closing quotation mark
            else if constexpr (CSH)
                C.AddCSH_ErrorAfter(loc, "Missing closing quotation mark.");
            else
                C.Error.Error(loc.start(),
                              "This opening quotation mark misses its closing pair. "
                              "Assuming string termination at line-end.",
                              "Quoted strings cannot have line breaks. Use \'\\n\' to insert a line break.");
            //Remove line endings from the string. Dot quoted strings may be multiline, but contain no linebreaks.
            if (text.find_first_of("\n\r") != text.npos) {
                std::string txt(text);
                std::erase_if(txt, [](char c) {return c == '\n' || c == '\r'; });
                val.multi_str.set_owning(std::move(txt));
            } else
                val.multi_str.set(text);
            return TOKEN::TOK_QSTRING;
        }
        default:
            _ASSERT(0);
            loc = inp.template commit<false, false>(len);
            return token;
        } //switch(token)
    }
}
}
