/*
    This file is part of Msc-generator.
    Copyright (C) 2008-2024 Zoltan Turanyi
    Distributed under GNU Affero General Public License.

    Msc-generator is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Msc-generator is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with Msc-generator.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <cstring>
#include "gvcsh.h"
#include "gvstyle.h"
#include "graph_parser_csh.h"

using namespace graph;

bool GraphCsh::DoHintLocated(EHintSourceType hsource, std::string_view a_name)
{
    if (hintStatus!=HINT_NONE)
        return true;
    if (hsource!=EHintSourceType::ATTR_VALUE || a_name!= "style")
        return Csh::DoHintLocated(hsource, a_name);
    //Style can be a comma-separated list of values. Limit the hinted string to the one we stand in the middle of.
    //We assume all such values are ASCII words
    _ASSERT(first_pos < hintedStringPos.first_pos);
    CshPos pos(cursor_pos, cursor_pos);
    while (hintedStringPos.first_pos < pos.first_pos)
        if (const char c = input_text[pos.first_pos.byte_index-1]
                ; ('a'<=c && c<='z') || ('A'<=c && c<='Z'))
            pos.first_pos = pos.first_pos.ShiftASCII(-1);
        else
            break;
    while (pos.end_pos<hintedStringPos.end_pos)
        if (const char c = input_text[pos.first_pos.byte_index+1]
                ; ('a'<=c && c<='z') || ('A'<=c && c<='Z'))
            pos.end_pos = pos.end_pos.ShiftASCII(1);
        else
            break;
    //now 'pos' contains the word under the cursor (may be empty)
    hintedStringPos = pos;
    return Csh::DoHintLocated(hsource, a_name);
}


GraphCsh::GraphCsh(Csh::FileListProc proc, const LanguageCollection* languages) :
    Csh(GraphContext(true, EContextParse::NORMAL, EContextCreate::PLAIN, FileLineCol()), proc, languages)
{
    FillNamesHints();
}

void GraphCsh::FillNamesHints()
{
    //these are used only when node definition is possible in AddCSH_KeywordOrEntity(), so
    //we do not add 'strict'
    AddOptionsToHints();
    MoveHintsToOptionNames();
    AddKeywordsOutsideGraph(true, true);  //not adding graph, node, subgraph, edge !!
    MoveHintsToKeywordNames();
    AttributeNames(GraphStyle::CLUSTER);
    AttributeNames(GraphStyle::GRAPH);
    AttributeNames(GraphStyle::SUBGRAPH);
    MoveHintsToOptionNames();
    AttributeNames(GraphStyle::ANY);
    MoveHintsToAttrNames();
}

/** Parse chart text for color syntax and hint info
 *
 * @param [in] input The chart text
 * @param [in] first_char The (1-based) index of the first character of 'input' in the file we parse.
 *             If we parse a whole file, it shall be 1.
 * @param [in] cursor_p The current position of the cursor.
 * @param [in] pedantic The initial value of the pedantic chart option.
   (We ignore it in CSH parsing.)*/
void GraphCsh::ParseText(std::string&& input, CharByteIndex first_char, CharByteIndex cursor_p, bool pedantic [[maybe_unused]] )
{
    //initialize data struct
    BeforeYaccParse(std::move(input), first_char, cursor_p);
    //call parsing
    sv_reader<true> reader(input_text, first_char);
    gv_parser_helper_csh proc_helper;
    graph_csh_parse(*this, proc_helper, reader); //return value ignored
    //Tidy up afterwards
    AfterYaccParse();
}

void GraphCsh::AddCSH_Subgraphname(const CshPos& pos, std::string_view name)
{
    //Subgraphs whose name starts with "cluster" are treated as a cluster
    if (name.starts_with("cluster")) {
        AddCSH(CshPos(pos.first_pos, pos.first_pos.ShiftASCII(7)), COLOR_ATTRVALUE_EMPH);
        AddCSH(CshPos(pos.first_pos.ShiftASCII(7), pos.end_pos), COLOR_ATTRVALUE);
    } else {
        AddCSH(pos, COLOR_ATTRVALUE);
    }
}

static const std::string_view compass_values[] = {"n", "ne", "e", "se", "s", "sw", "w", "nw", "c", "_", ""};
void GraphCsh::AddCSH_Compass(const CshPos & pos, std::string_view name)
{
    for (auto c : compass_values)
        if (name == c) {
            AddCSH(pos, COLOR_ATTRVALUE);
            return;
        }
    AddCSH_Error(pos, "Expecting a compass point: one of 'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'c', '_'.");
}

void GraphCsh::AddCSH_AttrName(const CshPos &pos, std::string_view name, EColorSyntaxType color)
{
    unsigned match_len=0;
    const DotAttributeDescription *aaa = nullptr;
    for (auto aa = DotAttributes; aa->name; aa++) {
        unsigned l = CaseInsensitiveCommonPrefixLen(aa->name, name);
        if (l>match_len) {
            aaa = aa;
            match_len = l;
        }
    }
    if (aaa==nullptr) return; //do not highlight
    if (match_len<strlen(aaa->name))
        AddCSH(pos, EColorSyntaxType(color+1));
    else
        AddCSH(pos, color);
}

/** Just color the text to COLOR_ATTRVALUE*/
void GraphCsh::AddCSH_AttrValue(const CshPos & pos, std::string_view aname, std::string_view avalue, EColorSyntaxType)
{
    if ("label_format"==aname || "link_format"==aname)
        AddCSH_AttrValue_CheckAndAddEscapeHint(pos, avalue, {});
     else
        AddCSH(pos, COLOR_ATTRVALUE);
}


void GraphCsh::AddCSH_LineBeginOutsideGraph(const CshPos & pos, std::string_view text)
{
    std::set<std::string> options = {"pedantic"};
    std::set<std::string> keywords = {"strict", "usedesign", "defcolor", "defstyle", "defdesign", "include", "defproc", "set"};

    const unsigned o = FindPrefix(options, text);
    const unsigned k = FindPrefix(keywords, text);
    const EColorSyntaxType color = o > k ? COLOR_OPTIONNAME : COLOR_KEYWORD;
    unsigned m = std::max(o, k);
    //Honor partial matches only if cursor is right after
    if (pos.end_pos != cursor_pos && m == 1)
        m = 0;
    switch (m) {
    case 2: AddCSH(pos, color); return;
    case 0: AddCSH_Error(pos, color == COLOR_OPTIONNAME ? "Unknown chart option." : "Unknown keyword."); return;
    case 1:
        AddCSH(pos, EColorSyntaxType(color+1));
        was_partial = true;
    }
}


void GraphCsh::AddLineBeginToHintsInsideDesignDef()
{
    static const char * const keyword_names[] = {"invalid", nullptr,
        "graph", "Set default attributes for all cluster subgraphs after this point.",
        "node", "Set default attributes for all nodes defined after this point.",
        "edge", "Set default attributes for all edges defined after this point.",
        "defcolor", "Define or change the meaning of a color name.",
        "defstyle", "Define or change a style.",
        "defproc", "Define a new procedure that can be re-played later.",
        "usedesign", "Apply settings of an existing design to the one under definition.",
        "include", "Include the text of another file in parsing.",
        ""
    };
    AttributeNames(GraphStyle::GRAPH);
    AddToHints(keyword_names, HintPrefix(COLOR_KEYWORD), EHintType::KEYWORD,
        CshHintGraphicCallbackForKeywords);
}

void GraphCsh::AddLineBeginToHintsInsideGraph()
{
    static const char * const keyword_names[] = {"invalid", nullptr,
        "graph", "Set default attributes for all cluster subgraphs after this point.",
        "node", "Set default attributes for all nodes defined after this point.",
        "edge", "Set default attributes for all edges defined after this point.",
        "subgraph", "Define a subgraph.",
        "cluster", "Define a visible subgraph. (Its name will be autogenerated or 'cluser_' will be prepended. This works only with the 'layout=dot' algorithm, which is the default.)",
        "defcolor", "Define or change the meaning of a color name.",
        "defproc", "Define a new procedure that can be re-played later.",
        "defstyle", "Define or change a style.",
        "set", "Define or set the value of a variable or change the value of a parameter inside a procedure.",
        "usedesign", "Apply a design to the remainder of this graph.",
        "replay", "Replay a previously defined procedure.",
        "include", "Include the text of another file in parsing.",
        "if", "Start a conditional statement. Useful only inside procedures.",
        ""
    };
    AddEntitiesToHints();
    AttributeNames(GraphStyle::GRAPH);
    AddToHints(keyword_names, HintPrefix(COLOR_KEYWORD), EHintType::KEYWORD,
        CshHintGraphicCallbackForKeywords);
    AllowAnything();
}

void GraphCsh::AddKeywordsOutsideGraph(bool add_strict, bool add_graph_digraph)
{
    static const char * const keyword_names1[] = {"invalid", nullptr,
        "graph", "Define an undirected graph. Use the '--' symbol for edges.",
        "digraph", "Define a directed graph. Use the '->' symbol for edges.",
        ""
    };
    static const char * const keyword_names2[] = {"invalid", nullptr,
        "strict", "Makes the following graph strict, which means that any two nodes can have only one edge between them.",
        "usedesign", "Apply a design to subsequent graphs.",
        "defstyle", "Define or change a style.",
        "defdesign", "Define a new design.",
        "set", "Define or set the value of a variable or change the value of a parameter inside a procedure.",
        "replay", "Replay a previously defined procedure.",
        "include", "Include the text of another file in parsing.",
        ""
    };
    if (add_strict)
        AddToHints(keyword_names2, HintPrefix(COLOR_KEYWORD), EHintType::KEYWORD,
            CshHintGraphicCallbackForKeywords);
    if (add_graph_digraph)
        AddToHints(keyword_names1, HintPrefix(COLOR_KEYWORD), EHintType::KEYWORD,
            CshHintGraphicCallbackForKeywords);
}


void GraphCsh::AddLineBeginToHintsOutsideGraph(bool add_strict, bool add_graph_digraph)
{
    if (add_strict)
        AddOptionsToHints();
    AddKeywordsOutsideGraph(add_strict, add_graph_digraph);
}

void GraphCsh::AddOptionsToHints()
{
    static const char * const option_names[] = {"invalid", nullptr,
        "pedantic", "If set, warnings are generated for edges not matching the directed/undirected nature of their graph.",
        ""
    };
    AddToHints(option_names, HintPrefix(COLOR_KEYWORD), EHintType::ATTR_NAME,
        CshHintGraphicCallbackForAttributeNames);
}

bool GraphCsh::AddOptionsValuesToHints()
{
    if (CaseInsensitiveEqual(hintAttrName, "pedantic")) {
        AddToHints(CshHint(HintPrefix(COLOR_ATTRVALUE) + "no",
            "Don't generate warnings for undirected edges in directed graphs and vice versa.",
            EHintType::ATTR_VALUE, true, CshHintGraphicCallbackForYesNo, 0));
        AddToHints(CshHint(HintPrefix(COLOR_ATTRVALUE) + "yes",
            "Generate warnings for directed edges in undirected graphs and vice versa.",
            EHintType::ATTR_VALUE, true, CshHintGraphicCallbackForYesNo, 1));
        return true;
    }
    return false;
}