/*
    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 steps.cpp The definition of FlowChart steps.
* @ingroup libflow_files */

#include "flowchart.h"

using namespace flow;

bool FlowElement::AddAttribute(const Attribute & a)
{
    if (a.Is("label")) {
        StringFormat basic = style.read().text;
        label = a.value;
        StringFormat::ExpandReferences(label, chart, a.linenum_value.start, &basic,
            false, true, StringFormat::LABEL, true);
        //re-insert position, so that FinalizeLabels has one
        //XXX label.insert(0, a.linenum_value.start.Print());
        return true;
    }
    return style.write().AddAttribute(a, chart);
}

void FlowElement::AttributeNames(Csh &csh)
{
    Element::AttributeNames(csh);
    csh.AddToHints(CshHint(csh.HintPrefix(COLOR_ATTRNAME) + "label",
        "Specify the text of the label of this element.",
        EHintType::ATTR_NAME));
}

bool FlowElement::AttributeValues(std::string_view attr, Csh &csh)
{
    if (CaseInsensitiveEqual(attr, "label")) 
        return true;
    return Element::AttributeValues(attr, csh);
}

FlowElement *FlowElement::AddAttributeList(AttributeList * al) 
{ 
    style.write().text += chart->MyCurrentContext().text;
    if (al) { 
        for (auto &pA: *al) 
            if (!AddAttribute(*pA))
                chart->Error.Error(pA->linenum_attr.start, "Attribute '"+pA->name+"' is not applicable here. Ignoring it."); 
        delete al; 
    } 
    return this; 
}

void FlowElement::PreLayout(Canvas & c) {
    parsed_label.Set(label, c, chart->Shapes, style.read().text); 
}

void FlowElement::FlowPostPosProcess(Canvas & c)
{
    chart->ExtendTotal(area.GetBoundingBox());
    Element::PostPosProcess(c, chart);
    if (!area.IsEmpty())
        chart->AllCovers += area;
}

Arrow::Arrow(FlowChart * f) : FlowElement(f)
{
    style = chart->MyCurrentContext().styles["arrow"];
}

FlowElement * Arrow::AddAttributeList(AttributeList * al)
{
    FlowElement::AddAttributeList(al);
    style.read().arrow.read().CreateArrowHeads();
    return this;
}

void Arrow::AttributeNames(Csh &csh)
{
    FlowElement::AttributeNames(csh);
    csh.AttributeNamesForStyle("arrow");
}

bool Arrow::AttributeValues(std::string_view attr, Csh &csh)
{
    if (FlowElement::AttributeValues(attr, csh)) return true;
    return csh.AttributeValuesForStyle(attr, "arrow");
}

void Arrow::Route()
{
    _ASSERT(!target.IsInvalid() && !source.IsInvalid());
    _ASSERT(target != source);
    _ASSERT(requested_source_port_side!=ELogicalDir::NDIR_INV);
    turns.clear();
    //'off' below indicates which side of the source column the arrow goes
    decided_source_port_side = requested_source_port_side;
    if (target.col == source.col) {
        if (target.element != source.element+1) {
            if (decided_source_port_side==ELogicalDir::NDIR) { //starts at bottom 
                const int off = -1; //left
                turns.emplace_back(source, 0,   1);
                turns.emplace_back(source, off, 1);
                turns.emplace_back(target, off, 0);
                decided_target_port_side = off<0 ? ELogicalDir::SDIR_INV : ELogicalDir::SDIR;
            } else if (decided_source_port_side==ELogicalDir::NDIR_INV) { //starts at top
                _ASSERT(0);
            } else { //starts at side
                const int off = decided_source_port_side == ELogicalDir::SDIR ? +1 : -1;
                turns.emplace_back(source, off);
                turns.emplace_back(target, off);
                decided_target_port_side = decided_source_port_side;
            }
            //else: no turns if target is simply next element from source
        }
    } else {
        if (decided_source_port_side==ELogicalDir::NDIR) {
            const int off = source.col<target.col ? +1 : -1; 
            turns.emplace_back(source, 0,   1);
            turns.emplace_back(source, off, 1);
            turns.emplace_back(target, -off, -1);
            turns.emplace_back(target, 0,   -1);
            decided_target_port_side = ELogicalDir::NDIR_INV;
        } else if (decided_source_port_side==ELogicalDir::NDIR_INV) { //starts at top
            _ASSERT(0);
        } else {
            if ((source.col<target.col)!=(decided_source_port_side==ELogicalDir::SDIR))
                decided_source_port_side = -decided_source_port_side;
            const int off = decided_source_port_side == ELogicalDir::SDIR ? +1 : -1;
            turns.emplace_back(source, off);
            //turns.emplace_back(target, -off, -1);
            turns.emplace_back(target, 0,    -1);
            decided_target_port_side = ELogicalDir::NDIR_INV;
        }
    }
}

void Arrow::FlowPostPosProcess(Canvas &canvas)
{
    const EDir sdir = chart->ConvertDir(decided_source_port_side);
    const EDir tdir = chart->ConvertDir(decided_target_port_side);
    std::vector<XY> p;
    p.push_back(chart->series[source.col].nodes[source.element].step->GetSidePoint(sdir));
    for (const auto &k : turns)
        p.push_back(chart->GetKneePoint(k));
    p.push_back(chart->series[target.col].nodes[target.element].step->GetSidePoint(tdir));
    //make it manhattan routing
    bool last_is_horizontal = true;
    for (size_t i = 1; i<p.size(); i++)
        if (p[i-1].x == p[i].x)
            last_is_horizontal = false;
        else if (p[i-1].y == p[i].y)
            last_is_horizontal = true;
        else if (last_is_horizontal)
            p.insert(p.begin()+i, XY(p[i].x, p[i-1].y));
        else
            p.insert(p.begin()+i, XY(p[i-1].x, p[i].y));
    path = Path(std::span<const XY>(p));
    path.Simplify();
    area = CoverPathWithArrows(arrowheads, style.read().arrow.read(), path, style.read().line); //this sets the cache
    chart->series[target.col].nodes[target.element].step->clip_by_arrows += arrowheads.TargetCoverEnd(style.read().arrow.read(), path, style.read().line);
    chart->series[source.col].nodes[source.element].step->clip_by_arrows += arrowheads.TargetCoverStart(style.read().arrow.read(), path, style.read().line);
    FlowElement::FlowPostPosProcess(canvas);
}

void Arrow::Draw(Canvas & c)
{
    DrawPathWithArrows(c, arrowheads, style.read().arrow.read(), path, style.read().line);
}

Step::Step(FlowChart * f, Type t) : FlowElement(f), type(t) 
{
    switch (t) {
    case flow::Step::BOX:
        style = chart->MyCurrentContext().styles["step"];
        break;
    case flow::Step::IF:
        style = chart->MyCurrentContext().styles["if"];
        break;
    case flow::Step::START:
        style = chart->MyCurrentContext().styles["start"];
        break;
    case flow::Step::STOP:
        style = chart->MyCurrentContext().styles["stop"];
        break;
    default:
        break;
    }
    prev_arrow = std::make_unique<Arrow>(f); //Capture style
}

void Step::AttributeNames(Csh &csh)
{
    FlowElement::AttributeNames(csh);
    csh.AttributeNamesForStyle("step");
}

bool Step::AttributeValues(std::string_view attr, Csh &csh)
{
    if (FlowElement::AttributeValues(attr, csh)) return true;
    return csh.AttributeValuesForStyle(attr, "step");
}



bool Step::AddToStepList(size_t col, std::unique_ptr<FlowElement>&& self)
{
    _ASSERT(self.get() == this);
    my_location.col = col;
    my_location.element = int(chart->series[col].nodes.size());
    //copy the saved arrow style to a previous step
    if (chart->series[col].nodes.size() &&
        chart->series[col].nodes.back().next==nullptr) {
        prev_arrow->target = my_location;
        prev_arrow->AddAttributeList(nullptr); //resolve text and arrow attrs.
        chart->series[col].nodes.back().next = std::move(prev_arrow);

    } else
        prev_arrow.release();
    chart->series[col].nodes.emplace_back();
    self.release();
    chart->series[col].nodes.back().step.reset(this);
    return true;
}

void Step::PreLayout(Canvas & canvas)
{
    FlowElement::PreLayout(canvas);
    if (parsed_label.IsEmpty())
        outer_edge = Block(0, 10, 0, 10);
    else
        outer_edge = Block(XY(0, 0), parsed_label.getTextWidthHeight()+
            style.read().line.LineWidth()*XY(1,1));
    center = outer_edge.Centroid();
    area = outer_edge;
}

XY Step::GetSidePoint(EDir d) const
{
    return BoxSideMidPoint(outer_edge, d);
}

void Step::Draw(Canvas & c)
{
    c.ClipInverse(clip_by_arrows);
    //if (!c.does_graphics()) we emit without clipping
    c.Entity(outer_edge, parsed_label, style.read(), chart->Shapes);
    c.UnClip();
}


bool Repeat::AddToStepList(size_t col, std::unique_ptr<FlowElement>&& self)
{
    _ASSERT(self.get()==this);
    self.release();
    target.element = int(chart->series[col].nodes.size());
    target.col = chart->ConvertToSeries(instructions, false);
    chart->series[col].nodes.back().branches.emplace_back(this);
    return false;
}

void Repeat::PreLayout(Canvas & canvas)
{ 
    FlowElement::PreLayout(canvas); 
}

bool Branch::AddToStepList(size_t col, std::unique_ptr<FlowElement>&& self)
{
    _ASSERT(self.get()==this);
    _ASSERT(chart->series[col].nodes.size());
    if (instructions.empty()) {
        chart->Error.Error(file_pos.start, "Branch needs some steps. Ignoring this.");
    } else {
        self.release();
        chart->series[col].nodes.back().branches.emplace_back(this);
        target.col = chart->ConvertToSeries(instructions);
        target.element = 0;
    }
    return true;
}

void Branch::PreLayout(Canvas & canvas)
{ 
    FlowElement::PreLayout(canvas); 
}

bool Goto::AddToStepList(size_t col, std::unique_ptr<FlowElement>&& self)
{
    _ASSERT(self.get()==this);
    if (chart->series[col].nodes.size()==0) {
        //create empty step
        chart->series[col].nodes.emplace_back();
    }
    self.release();
    chart->series[col].nodes.back().next.reset(this);
    //dont fill target yet: at this point not all steps are added to series
    return false;
}


/** Returns list size if laid horizontall or vertically, resp.*/
void StepList::Layout(EDir d)
{
    dir = d;
    size = Block(0, 0, 0, 0);
    xyPos = XY(0, 0);
    for (auto &n: nodes) {
        const XY half_size = n.step->outer_edge.Spans()/2;
        const XY label_size = n.next ? n.next->parsed_label.getTextWidthHeight() : XY(0, 0);
        double gap = n.step->chart->defaultGap + 
            (IsHorizontal(dir) ? label_size.x : label_size.y);
        switch (d) {
        case flow::EDir::UP:
            n.step->ShiftTo(XY(0, size.y.from - half_size.y));
            size.x += -half_size.x;
            size.x += half_size.x;
            size.y.from -= half_size.y*2 + gap;
            break;
        case flow::EDir::DOWN:
            n.step->ShiftTo(XY(0, size.y.till + half_size.y));
            size.x += -half_size.x;
            size.x += half_size.x;
            size.y.till += half_size.y*2 + gap;
            break;
        case flow::EDir::LEFT:
            n.step->ShiftTo(XY(size.x.from - half_size.x, 0));
            size.y += -half_size.y;
            size.y += half_size.y;
            size.x.from -= half_size.x*2 + gap;
            break;
        case flow::EDir::RIGHT:
            n.step->ShiftTo(XY(size.x.till + half_size.x, 0));
            size.y += -half_size.y;
            size.y += half_size.y;
            size.x.till += half_size.x*2 + gap;
            break;
        default:
            break;
        }
    }
}

void StepList::ShiftNodesTo(XY pos)
{
    pos -= xyPos; //becomes delta
    for (auto &fn : nodes)
        fn.step->ShiftBy(pos);
    size.Shift(pos);
    xyPos += pos;
}

void FlowNode::FillInSources() const
{
    _ASSERT(step);
    if (next) {
        next->source = step->my_location;
        next->requested_source_port_side = ELogicalDir::NDIR;
    }
    _ASSERT(branches.size()<=2);
    for (auto &a:branches) {
        a->source = step->my_location;
        a->requested_source_port_side = &a == &branches.front() ? ELogicalDir::SDIR : ELogicalDir::SDIR_INV;
    }
}

void FlowNode::Route()
{
    if (next)
        next->Route();
    for (auto &a: branches)
        a->Route();
}

void FlowNode::FlowPostPosProcess(Canvas &canvas)
{
    step->FlowPostPosProcess(canvas);
    if (next)
        next->FlowPostPosProcess(canvas);
    for (auto &a : branches)
        a->FlowPostPosProcess(canvas);
}

void FlowNode::Draw(Canvas & canvas)
{
    step->Draw(canvas);
    if (next)
        next->Draw(canvas);
    for (auto &a : branches)
        a->Draw(canvas);
}

