/*
    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/>.
*/
/** @file dbgchart.cpp The definition for the DbgChart class and all of the "dbg" language.
* @ingroup libdbg_files */

/** @defgroup libdbg The engine of the dbg library
@ingroup libs

 More documentation is coming.

*/

/** @defgroup libdbg_files Files for the dbg library.
* @ingroup libdbg*/


#include "canvas.h"
#include "dbgchart.h"

using namespace dbg;

std::unique_ptr<Csh> DbgChart::CshFactory(Csh::FileListProc, const LanguageCollection* languages) const
{
    return std::make_unique<DummyCsh>(languages);
}

std::map<std::string, std::string> DbgChart::RegisterLibraries() const
{
    //Add any libraries you use here (no need to add cairo),
    //like "mylib v5.0.0" in UTF-8
    return {};
}



DbgChart::DbgChart(FileReadProcedure *p, void *param, const LanguageCollection* languages) : ChartBase(p, param, languages)
{
    Contexts.emplace_back(true, EContextParse::NORMAL, EContextCreate::PLAIN, FileLineCol());  //creates new, plain context
}

DbgChart::~DbgChart()
{
    //Your own destructor
}

class Parser
{
    std::string_view input;
    const char* pos1;
    FileLineCol pos2;
    void update_pos() { pos2.col += input.data() - pos1; pos1 = input.data(); }
public:
    explicit Parser(std::string_view input, FileLineCol start) : input(input), pos1(input.data()), pos2(start) {}
    bool empty() const { return input.empty(); }
    operator std::string_view() const { return input; }
    FileLineCol GetPos() { update_pos(); return pos2; }

    bool consume_newline() {
        if (!input.starts_with('\n')) return false;
        input.remove_prefix(1);
        pos1 = input.data();
        pos2.line++;
        pos2.col = 1;
        return true;
    }

    template <typename ...Strings>
    std::pair<FileLineCol, int> find(Strings... strings) {
        std::pair<FileLineCol, int> ret = { pos2, 0 };
        while (!ret.second && input.size()) {
            int i = 1;
            ((input.starts_with(strings) && ret.second == 0 ? (ret = { GetPos(), i}, (std::string_view(strings)=="\n" ? (void)consume_newline() : input.remove_prefix(std::string_view(strings).size()))) : (void)0, i++), ...);
            if (!ret.second && !consume_newline()) input.remove_prefix(1);
        }
        return ret;
    }
    //Returns the number of newlines skipped
    int skip_space() {
        int ret = -1;
        do {
            while (input.size() && (input.front() == ' ' || input.front() == '\t'))
                input.remove_prefix(1);
            ++ret;
        } while (consume_newline());
        return ret;
    }

    bool skip(std::string_view what) {
        skip_space();
        if (input.starts_with(what)) {
            input.remove_prefix(what.size());
            skip_space();
            return true;
        }
        return false;
    }

    std::optional<double> parseNum() {
        double ret;
        if (input.starts_with("0x")) {
            //This should not be necessary, but MS from_chars do not recognize 0x as hex
            if (auto r1 = std::from_chars(input.data() + 2, input.data() + input.size(), ret, std::chars_format::hex); r1.ec == std::errc{}) {
                input.remove_prefix(r1.ptr - input.data());
                return ret;
            }
        } else if (input.starts_with("-0x")) {
            //This should not be necessary, but MS from_chars do not recognize 0x as hex
            if (auto r1 = std::from_chars(input.data() + 3, input.data() + input.size(), ret, std::chars_format::hex); r1.ec == std::errc{}) {
                input.remove_prefix(r1.ptr - input.data());
                return -ret;
            }
        } else if (auto r1 = std::from_chars(input.data(), input.data() + input.size(), ret); r1.ec == std::errc{}) {
            input.remove_prefix(r1.ptr - input.data());
            return ret;
        }
        return {};
    }

    std::optional<XY> parseXY(bool curly, bool allow_member_names) {
        skip(curly ? "{" : "XY(");
        if (allow_member_names) skip("x"), skip("=");
        auto x = parseNum();
        if (!x) return {};
        if (!skip(",")) return {};
        if (allow_member_names) skip("y"), skip("=");
        auto y = parseNum();
        if (!y) return {};
        skip(curly ? "}" : ")");
        return XY{ *x, *y };
    }

    //+		[9]	{{290.2, 528.521}->{290.2, 500.621} (marked)}	contour::Edge
    //+		[9]	{{290.2, 528.521}->{290.2, 500.621} via {290.2, 500.621}, {290.2, 500.621} (invisible) (marked)}	contour::Edge
    std::optional<Edge> parseVCEdge() {
        auto s = parseXY(true, false);
        if (!s) return {};
        if (!skip("->")) return {};
        auto e = parseXY(true, false);
        if (!e) return {};
        Edge ret;
        if (skip("via")) {
            auto c1 = parseXY(true, false);
            if (!c1) return {};
            skip(",");
            auto c2 = parseXY(true, false);
            if (!c2) return {};
            ret = Edge(*s, *e, *c1, *c2);
        } else
            ret = Edge(*s, *e);
        if (skip("(invisible)")) ret.SetVisible(false);
        if (skip("(marked)")) ret.SetInternalMark(true);
        skip("}");
        skip("contour::Edge");
        return ret;
    }


    //Edge(XY(244.452010, 125.997362), XY(256.167193, 103.138561), XY(250.454638, 118.711733), XY(254.238430, 111.198503)),
    //Edge(XY(256.167193, 103.138561), XY(259.571087, 103.953120), true, 1),
    std::optional<Edge> parseDumpedEdge() {
        auto s = parseXY(false, false);
        if (!s) return {};
        if (!skip(",")) return {};
        auto e = parseXY(false, false);
        if (!e) return {};
        if (skip(")")) return Edge(*s, *e);
        if (!skip(",")) return {};
        Edge ret;
        if (auto c1 = parseXY(false, false); c1) {
            if (!skip(",")) return {};
            if (auto c2 = parseXY(false, false); !c2) return {};
            else ret = Edge(*s, *e, *c1, *c2);
        } else
            ret = Edge(*s, *e);
        if (skip(")")) return ret;
        skip(","); // We may come here either with an already consumed comma or not 
        if (skip("true")) ret.SetVisible(true);
        else if (skip("false")) ret.SetVisible(false);
        else return {};
        if (skip(",")) {
            if (skip("1")) ret.SetInternalMark(true);
            else if (skip("0")) ret.SetInternalMark(false);
            else if (skip("true")) ret.SetInternalMark(true);
            else if (skip("false")) ret.SetInternalMark(false);
            else return {};
        }
        skip(")");
        skip(",");
        return ret;
    }


     //(const contour::Edge &) @0x55555601b890: {
     // <contour::EdgeStorage> = {
     //   <contour::EdgeBase> = {
     //     static MAX_CP = 9,
     //     static MARK_BIT_WIDTH = 23,
     //     static flatness_tolerance = 0.001
     //   },
     //   members of contour::EdgeStorage:
     //   start_ = {
     //     x = 70.394403040569458,
     //     y = 97.695731568266297
     //   },
     //   end_ = {
     //     x = 70.425179215216815,
     //     y = 97.669078716958595
     //   },
     //   c1_ = {
     //     x = 70.40468477608033,
     //     y = 97.686860834112977
     //   },
     //   c2_ = {
     //     x = 70.414943489580054,
     //     y = 97.677976525352776
     //   },
     //   meta_ = {
     //     mark_ = 0,
     //     imark_ = 0,
     //     visible_ = 1,
     //     straight = 0,
     //     has_cp = 1,
     //     connects_to_previous = 0
     //   }
     // }, <No data fields>}
    std::optional<Edge> parseGDBEdge() {
        if (1 != find("start_").second) return {};
        skip("=");
        auto s = parseXY(true, true);
        if (!s) return {};
        if (!skip(",")) return {};
        if (1 != find("end_").second) return {};
        skip("=");
        auto e = parseXY(true, true);
        if (!e) return {};
        if (!skip(",")) return {};
        if (1 != find("c1_").second) return {};
        skip("=");
        auto c1 = parseXY(true, true);
        if (!c1) return {};
        if (!skip(",")) return {};
        if (1 != find("c2_").second) return {};
        skip("=");
        auto c2 = parseXY(true, true);
        if (!c2) return {};

        if (1 != find("imark_").second) return {};
        skip("=");
        bool imark;
        if (input.starts_with("0")) imark = false;
        else if (input.starts_with("1")) imark = true;
        else return {};

        if (1 != find("visible_").second) return {};
        skip("=");
        bool visible;
        if (input.starts_with("0")) visible = false;
        else if (input.starts_with("1")) visible = true;
        else return {};

        if (1 != find("straight").second) return {};
        skip("=");
        bool straight;
        if (input.starts_with("0")) straight = false;
        else if (input.starts_with("1")) straight = true;
        else return {};

        find("<No data fields>}"); //go to the end
        if (straight) return Edge(*s, *e, visible, imark);
        return Edge(*s, *e, *c1, *c2, visible, imark);
    }
};

void DbgChart::ParseText(std::string_view input, const FileLineCol& first_char)
{
    if (input.empty()) return;
    Parser P(input, first_char);

    paths = std::vector<DbgPath>(1);
    LineAttr line(ELineType::SOLID, ColorType::black(), 1);
    XY off(0, 0);
    P.skip_space();
    while (!P.empty()) {
        auto [begin, res] = P.find("\n", "{", "Edge(", "<contour::EdgeStorage>", 
                                   "center=", "size=", "zoom=", "lw=", "color=", "off=");
        switch (res) {
        case 1:
            if (P.skip_space())  //if nonzero, we skipped empty lines
                if (!paths.back().IsEmpty())
                    paths.emplace_back();
            break;
        case 2:
            if (auto e = P.parseVCEdge(); e) {
                paths.back().emplace_back(*e, line, off);
                elements.emplace_back(this, *e, FileLineColRange(begin, P.GetPos()));
            }
            break;
        case 3:
            if (auto e = P.parseDumpedEdge(); e) {
                paths.back().emplace_back(*e, line, off);
                elements.emplace_back(this, *e, FileLineColRange(begin, P.GetPos()));
            }
            break;
        case 4:
            if (auto e = P.parseGDBEdge(); e) {
                paths.back().emplace_back(*e, line, off);
                elements.emplace_back(this, *e, FileLineColRange(begin, P.GetPos()));
            }
            break;
        case 5: //center
            if (auto c = P.parseXY(true, true)) {
                center = *c;
                P.skip(";");
            } else
                Error.Error(P.GetPos(), "Expected a coordinate in curly braces after 'center'.");
            break;
        case 6: //size
            if (auto z = P.parseNum()) {
                size = *z;
                P.skip(";");
            } else
                Error.Error(P.GetPos(), "Expected a number after 'size'.");
            break;
        case 7: //zoom
            if (auto z = P.parseNum()) {
                zoom = *z;
                P.skip(";");
            } else
                Error.Error(P.GetPos(), "Expected a number after 'zoom'.");
            break;
        case 8: //lw
            if (auto z = P.parseNum()) {
                line.width = *z;
                P.skip(";");
            } else
                Error.Error(P.GetPos(), "Expected a number after 'lw'.");
            break;
        case 9: //color
            if (auto z = P.parseNum()) {
                static constexpr std::array colors = { ColorType::red(), ColorType::green(), ColorType::blue(), ColorType::black(), ColorType::gray() };
                if (int(*z) == *z && 0 <= *z)
                    line.color = *z ? colors[int(*z) % colors.size()] : ColorType::none();
                else
                    Error.Error(P.GetPos(), "Expected a nonnegative integer after 'color'.");
                P.skip(";");
            } else
                Error.Error(P.GetPos(), "Expected a number after 'zoom'.");
            break;
        case 10: //off
            if (auto c = P.parseXY(true, true)) {
                off = *c;
                P.skip(";");
            } else
                Error.Error(P.GetPos(), "Expected a coordinate in curly braces after 'off'.");
            break;
        default:
            break;
        }
    }
}

void DbgChart::CompleteParse(bool, bool, XY, bool, bool)
{
    Canvas canvas(Canvas::Empty::Query);
    pageBreakData.clear();
    //Consider the copyright text
    StringFormat sf;
    sf.Default();
    XY crTexSize = Label(copyrightText, canvas, Shapes, sf).getTextWidthHeight().RoundUp();
    copyrightTextHeight = crTexSize.y;

    total.MakeInvalid();
    if (zoom!=1) {
        for (auto& p : paths) 
            p.Scale(zoom);
        for (auto &e : elements)
            e.Scale(zoom);
        AllCovers.Scale(zoom);
        if (center)
            *center *= zoom;
    }
    for (auto& p : paths)
        total += p.CalculateBoundingBox();
   
    if (total.IsInvalid())
        total = Block(0, 1, 0, 1); //have at least this size
    else if (center) 
        total = total * Block(*center-XY(500,500), *center+XY(500,500));
    else
        total.Expand(10);
    if (total.x.till < crTexSize.x) total.x.till = crTexSize.x;
    if (pageBreakData.size()==0)
        pageBreakData.emplace_back(XY(0, 0), XY(0, 0)); //we *must* have one page
    //We keep graphs in the force_collapse list even if they do not really exist.

    Error.Sort();
}


void DbgChart::CollectIsMapElements(Canvas & /*canvas*/)
{
}

void DbgChart::RegisterAllLabels()
{
}

void DbgChart::Draw(Canvas & c, bool pageBreaks [[maybe_unused]], unsigned page [[maybe_unused]])
{
    c.Fill(total, FillAttr::Solid(ColorType::white()));
    for (auto& p : paths)
        for (auto e : p) {
            e.line.type = e.IsVisible()
                ? (e.IsInternalMarked() ? ELineType::DASHED : ELineType::SOLID)
                : (e.IsInternalMarked() ? ELineType::DASH_DOT : ELineType::DOTTED);
            e.Shift(e.off);
            c.Line(e, e.line);
        }
    bkFill.emplace_back(FillAttr::Solid(ColorType::white()), total);
}


void DbgChart::SetToEmpty()
{
    //Your own code to clear all internal data and create a chart that shows an empty page
    //(same as if you had parsed the empty file)
}