/*
    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 flowchart.cpp The definition for the FlowChart class and all of the "flow" language.
* @ingroup libflow_files */

/** @defgroup libflow The engine of the flow library
@ingroup libs

 More documentation is coming.

*/

/** @defgroup libflow_files Files for the flow library.
* @ingroup libflow*/


#include "canvas.h"
#include "flowchart.h"
#include "flowcsh.h"
#include "steps.h"
#include "flow_parser_compile.h"

using namespace flow;


std::string FlowChart::GetLanguageDefaultText() const {
    return
        "#This is the default flowchart text\n"
        "start;\n"
        "turn box : turn handle ?;\n"
        "if: window open;\n"
        "then { stop; }\n"
        "else goto turn;\n";
}

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

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

std::unique_ptr<FlowElement> FlowChart::PopContext()
{
    Contexts.pop_back();
    //you may return an instruction if needed. It will be the last instruction
    //in the list of instructions for this scope
    return {};
}


bool FlowChart::AddCommandLineArg(const std::string & /*arg*/)
{
    return false;
}

void FlowChart::AddCommandLineOption(const Attribute & /*a*/)
{
}

FlowElement *FlowChart::AddAttribute(const Attribute &a)
{
    if (AddDesignAttribute(a)) return nullptr;
    //TODO: Apply chart options here that do not apply to designs
    return nullptr; //you may return an instruction if the option results in one
}

bool FlowChart::AddDesignAttribute(const Attribute&)
{
    //TODO: Apply chart options here that also apply to designs
    return false; //return true if you have recognized & handled the chart option
}



bool FlowChart::DeserializeGUIState(std::string_view)
{
    return true;
}

std::string FlowChart::SerializeGUIState() const
{
    return std::string();
}

bool FlowChart::ControlClicked(Element*, EGUIControlType)
{
    return false;
}

bool FlowChart::ApplyForcedDesign(const string & name)
{
    //this is called before parsing
    auto i = Designs.find(name);
    if (i==Designs.end())
        return false;
    ignore_designs = true;
    Contexts.back().ApplyContextContent(i->second);
    return Chart::ApplyForcedDesign(name);
}

/** Adds a chart option. Currently this is only 'pedantic'.
 * If not a chart option, try adding it to the graph/subgraph we are in. */
 bool FlowChart::AddChartOption(const Attribute &a)
{
    if (a.Is("flow")) {
        //add your chart option to MyCurrentContext()
        return true;
    }
    //Error if not recognized
    Error.Error(a, false, "Unrecognized chart option. Ignoring it.");
    return false;
}

void FlowChart::ParseText(std::string_view input, const FileLineCol& first_char)
{
    //if you support progres::
    //Progress.RegisterParse(len);
    sv_reader<false> reader(input, first_char);
    procedure_parse_helper proc_helper;
    flow_compile_parse(*this, proc_helper, reader); //return value ignored
}


void FlowChart::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;

    //Transform to lists
    ConvertToSeries();

    //Arrange blocks
    Layout();

    //Reorder columns to minimize arrows crossing columns
    BestOrdering();

    //Layout again
    Layout();

    //PostPosProcess
    PostPosProcess();

    total += Block(0, 1, 0, 1); //have at least this size
    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 FlowChart::BestOrdering() {
    std::vector<size_t> map(series.size()), found; //map[i] tells the current index of column ID 'i'
    for (size_t u = 0; u<series.size(); u++) map[u] = u;
    struct Diff {
        size_t diff=0, sub_diff=0;
        constexpr auto operator <=>(const Diff&) const noexcept = default;
        Diff& operator +=(const Diff& o) noexcept { diff += o.diff; sub_diff += o.sub_diff; return *this; }
        constexpr static Diff max() noexcept { return {std::numeric_limits<size_t>::max(), std::numeric_limits<size_t>::max()}; }
        constexpr bool is_zero() const noexcept { return diff==0 && sub_diff==0; }
    };
    const auto crossing = [&map, this](const Arrow& a)->Diff {
        ptrdiff_t c1 = a.source.col, c2 = a.target.col;
        _ASSERT(c1>=0 && c1<(int)map.size());
        _ASSERT(c2>=0 && c2<(int)map.size());
        c1 = map[c1]; c2 = map[c2];
        Diff ret;
        if (a.requested_source_port_side == ELogicalDir::SDIR     && c2<c1) ret.sub_diff++;
        if (a.requested_source_port_side == ELogicalDir::SDIR_INV && c1<c2) ret.sub_diff++;
        //count how many columns would this arrow cross
        if (a.turns.empty()) {
            const ptrdiff_t diff = abs(c1-c2);
            ret.diff = diff<=1 ? 0 : diff-1;
        } else {
            const bool dim = IsHorizontal(sdir); //the dimension of the extent we cross
            const double Y = GetKneePoint(a.turns.front())[dim];
            for (ptrdiff_t col = std::min(c1, c2)+1; col<std::max(c1, c2); col++)
                if (series[col].size[dim].IsWithinBool(Y))
                    ret.diff++;
        }
        return ret;
    };
    Diff min_crossing = Diff::max();
    do {
        Diff sum_crossing;
        for (auto& sl : series)
            for (auto& n : sl.nodes)
                for (auto& a : n.branches)
                    if (min_crossing < (sum_crossing += crossing(*a)))
                        goto finish;
        if (sum_crossing<min_crossing) {
            min_crossing = sum_crossing;
            found = map;
        }
    finish:
        if (min_crossing.is_zero()) break; //cannot get lower
    } while (std::next_permutation(map.begin(), map.end()));
    //translate the arrow indices
    for (auto& sl : series)
        for (auto& n : sl.nodes) {
            for (auto& a : n.branches) {
                if (a->source.col>=0) a->source.col = found[a->source.col];
                if (a->target.col>=0) a->target.col = found[a->target.col];
            }
            if (const auto a = n.next.get()) {
                if (a->source.col>=0) a->source.col = found[a->source.col];
                if (a->target.col>=0) a->target.col = found[a->target.col];
            }
        }
    //Now re-order the columns
    //start by inverting the mapping in 'found' into 'map'
    for (size_t u = 0; u<series.size(); u++) map[found[u]] = u;
    //map[i] now tells us the original column ID of the column
    //that goes to the ith index (col) in the new order
    std::vector<StepList> new_series;
    new_series.reserve(series.size());
    for (size_t col : map)
        new_series.push_back(std::move(series[col]));
    series = std::move(new_series);
}

size_t FlowChart::ConvertToSeries(FElementList &list, bool allocate_new)
{
    //Go through the list and sanitize branches
    for (auto i = list.begin(); i!=list.end(); ++i)
        if (Branch* p = dynamic_cast<Branch*>(i->get()))
            if (p && p->instructions.empty() && std::next(i) != list.end()
                    && dynamic_cast<Branch*>(std::next(i)->get()) == nullptr)
                //An empty branch instruction followed by a non-branch one
                p->instructions.splice(p->instructions.begin(), list,
                                       std::next(i), list.end());
        //empty branch instructions followed by nothing or other branch
        //instructions remain empty and will trigger an error in Branch::AddStepList() below.

    _ASSERT(allocate_new || series.size());
    size_t sl = allocate_new ? NewStepList() : series.size()-1;
    size_t ret = sl;
    for (auto &s : list) {
        bool cont = s->AddToStepList(sl, std::move(s));
        if (cont) continue;
        else sl = NewStepList();
    }
    return ret;
}

void FlowChart::ConvertToSeries()
{
    ConvertToSeries(steps);
    steps.clear(); //some elements have remained that caused Error.
    //santitize the series
    //- all Arrow::target is filled in and valid (or point to next in steplist)
    for (auto &sl : series)
        for (auto &n : sl.nodes)
            if (n.next && n.next->target.IsInvalid()) {
                Goto *g = dynamic_cast<Goto*>(n.next.get());
                if (g) {
                    n.next->target = FindRefNameInSeries(g->target_str);
                    if (n.next->target.IsInvalid()) {
                        Error.Error(n.next->file_pos.start, "Reference '"+g->target_str+"' is not used on any step. Ignoring 'goto statement'.");
                        n.next.reset();
                    }
                } else {
                    _ASSERT(0); //we should have all other targets set in ConvertToSeries()
                                //or invalid target mean 'next in series'
                }
            }
    //- no nullptr 'step's
    for (auto &sl : series)
        for (unsigned u = 0; u<sl.nodes.size(); /*nope*/) {
            if (sl.nodes[u].step) {
                u++;
            } else {
                if (sl.nodes[u].next)
                    DeleteItemFromSeries(NodeRef(&sl-&series.front(), u), sl.nodes[u].next->target);
                else
                    DeleteItemFromSeries(NodeRef(&sl-&series.front(), u), NodeRef());
            }
        }
    //- remove empty columns
    for (unsigned u = 0; u<series.size(); /*nope*/)
        if (series[u].nodes.size())
            u++;
        else
            DeleteColumnFromSeries(u);
    //- fill in all the Arrow::sources field
    for (auto &sl : series)
        for (auto &n : sl.nodes)
            n.FillInSources();
    //- remove duplicate arrows
    for (auto &sl : series)
        for (auto &n : sl.nodes)
            for (unsigned u=0; u<n.branches.size(); u++)
                for (unsigned v = u+1; v<n.branches.size(); /*nope*/) {
                    if (n.branches[u]->target == n.branches[v]->target) {
                        Error.Error(n.branches[v]->file_pos.start,
                            "This branch leads to the same destination as another one. Ignoring it.");
                        Error.Error(n.branches[u]->file_pos.start, n.branches[v]->file_pos.start,
                            "This is the duplicate branch we keep.");
                        n.branches.erase(n.branches.begin()+v);
                    } else {
                        v++;
                    }
                }
    //-order column to minimize arrows crossing columns

}

NodeRef FlowChart::FindRefNameInSeries(const std::string & refname) const
{
    for (auto &sl : series)
        for (auto &n : sl.nodes)
            if (n.step && n.step->refname==refname)
                return NodeRef(&sl-&series.front(), &n-&sl.nodes.front());
    return NodeRef();
}

void FlowChart::DeleteItemFromSeries(NodeRef t, NodeRef replaceto)
{
    _ASSERT(t.col < (int)series.size());
    _ASSERT(t.element < series[t.col].nodes.size());
    _ASSERT(replaceto.col < (int)series.size());
    _ASSERT(replaceto.element < series[replaceto.col].nodes.size());

    auto adjust = [t, replaceto](Arrow& a) {
        if (a.target.col != t.col) return;
        if (a.target.element > t.element)
            a.target.element--;
        else if (a.target.element == t.element) {
            a.target = replaceto;
            //TODO: combine two gotos (e.g., label & style...)
        }
    };

    for (auto &sl : series)
        for (auto &n : sl.nodes) {
            if (n.next) adjust(*n.next);
            for (auto& a: n.branches)
                adjust(*a);
        }
    series[t.col].nodes.erase(series[t.col].nodes.begin() + t.element);
}

void FlowChart::DeleteColumnFromSeries(int col)
{
   _ASSERT(col>=0 && col<(int)series.size());
    for (auto &sl : series)
        for (auto &n : sl.nodes) {
            for (auto& a: n.branches)
                if (a->target.col > col)
                    a->target.col--;
            if (n.next) {
                _ASSERT(n.next->target.col != col);
                if (n.next->target.col > col)
                    n.next->target.col--;
            }
        }
    series.erase(series.begin() + col);
}


void FlowChart::Layout() {
    if (series.size()==0) {
        total = Block(0, 0, 0, 0);
        return;
    }
    _ASSERT(IsHorizontal(sdir)!=IsHorizontal(ndir)); //ndir and sdir must be perpendicular
    Canvas canvas(Canvas::Empty::Query);
    //Calculate node sizes
    for (auto& sl : series)
        for (auto& fn : sl.nodes) {
            fn.step->PreLayout(canvas);
            if (fn.next) fn.next->PreLayout(canvas);
            for (auto& a : fn.branches)
                a->PreLayout(canvas);
        }
    //Place all columns side-by-side
    XY p(0, 0);
    for (auto& col : series) {
        col.Layout(ndir);
        p += col.size.Spans().Scale(EDirOffset(sdir)/2);
        col.ShiftNodesTo(p);
        p += col.size.Spans().Scale(EDirOffset(sdir)/2);
        p += defaultGap * EDirOffset(sdir);
    }
    //Shift down each col down to where it starts
    const bool dim = !IsHorizontal(ndir);
    const bool inc = IsIncreasing(ndir);
    bool changed;
    do {
        changed = false;
        for (size_t col = 0; col<series.size(); col++) {
            for (auto& fn : series[col].nodes) {
                const double at_least = fn.step->center[dim] + (inc ? arrowGap : -arrowGap);
                for (auto& a : fn.branches)
                    if (a->target.col!=int(col) && a->IsBranch()) {
                        auto& tcol = series[a->target.col];
                        const double pos = tcol.xyPos[dim];
                        if (inc && at_least<=pos) continue;
                        if (!inc && at_least>=pos) continue;
                        XY to = tcol.xyPos;
                        to[dim] = at_least;
                        tcol.ShiftNodesTo(to);
                        changed = true;
                    }
            }
        }
    } while (changed);
    //Route arrows
    for (auto& sl : series)
        for (auto& fn : sl.nodes)
            fn.Route();

    total = series.front().size;
    total += series.back().size;
}

XY flow::FlowChart::GetKneePoint(const KneePos & k) const
{
    XY ret;
    const Step* const step = series[k.col.elem].nodes[k.element.elem].step.get();
    const bool dim = IsHorizontal(sdir);  //X or Y dimension: 1 if sdir is RIGHT or LEFT

    const bool s_fw = IsIncreasing(sdir);   //true if series dir (columns) is RIGHT or DOWN
    const Range& col_width = step->outer_edge[!dim];
    if (k.col.side<0) ret[!dim] = s_fw ? col_width.from - arrowGap : col_width.till + arrowGap; // cppcheck-suppress uninitvar
    else if (0<k.col.side) ret[!dim] = s_fw ? col_width.till + arrowGap : col_width.from - arrowGap;
    else ret[!dim] = step->center[!dim];

    const bool n_fw = IsIncreasing(ndir); //true if node dir (elements in a col) is RIGHT or DOWN
    const Range& elem_height = step->outer_edge[dim];
    if (k.element.side<0) ret[dim] = n_fw ? elem_height.from - arrowGap : elem_height.till + arrowGap;
    else if (0<k.element.side) ret[dim] = n_fw ? elem_height.till + arrowGap : elem_height.from - arrowGap;
    else ret[dim] = step->center[dim];

    return ret;
}

void FlowChart::PostPosProcess()
{
    Canvas canvas(Canvas::Empty::Query);
    for (auto &sl : series)
        for (auto &n : sl.nodes)
            n.FlowPostPosProcess(canvas);
    total.Expand(10);
}

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

void FlowChart::RegisterAllLabels()
{
}

void FlowChart::Draw(Canvas & c, bool pageBreaks, unsigned page)
{
    if (page) {
        //draw page X
    } else {
        //draw background

        if (pageBreaks)
            DrawPageBreaks(c);

        //draw all pages
        for (auto &sl : series)
            for (auto &n : sl.nodes)
                n.Draw(c);
    }
}

void FlowChart::SetToEmpty()
{
    ChartBase<FlowContext>::SetToEmpty();
    pageBreakData.emplace_back(XY(0, 0), XY(0, 0));
    steps.clear();
    series.clear();
}


