//Copyright (c) 2021 Ultimaker B.V.
//CuraEngine is released under the terms of the AGPLv3 or higher.

#include "TreeNode.hpp"

#include "../../Geometry.hpp"

namespace Slic3r::FillLightning {

coord_t Node::getWeightedDistance(const Point& unsupported_location, const coord_t& supporting_radius) const
{
    constexpr coord_t min_valence_for_boost = 0;
    constexpr coord_t max_valence_for_boost = 4;
    constexpr coord_t valence_boost_multiplier = 4;

    const size_t valence = (!m_is_root) + m_children.size();
    const coord_t valence_boost = (min_valence_for_boost < valence && valence < max_valence_for_boost) ? valence_boost_multiplier * supporting_radius : 0;
    const auto dist_here = coord_t((getLocation() - unsupported_location).cast<double>().norm());
    return dist_here - valence_boost;
}

bool Node::hasOffspring(const NodeSPtr& to_be_checked) const
{
    if (to_be_checked == shared_from_this())
        return true;

    for (auto& child_ptr : m_children)
        if (child_ptr->hasOffspring(to_be_checked)) 
            return true;

    return false;
}

NodeSPtr Node::addChild(const Point& child_loc)
{
    assert(m_p != child_loc);
    NodeSPtr child = Node::create(child_loc);
    return addChild(child);
}

NodeSPtr Node::addChild(NodeSPtr& new_child)
{
    assert(new_child != shared_from_this());
    //assert(p != new_child->p); // NOTE: No problem for now. Issue to solve later. Maybe even afetr final. Low prio.
    m_children.push_back(new_child);
    new_child->m_parent = shared_from_this();
    new_child->m_is_root = false;
    return new_child;
}

void Node::propagateToNextLayer(
    std::vector<NodeSPtr>& next_trees,
    const Polygons& next_outlines,
    const EdgeGrid::Grid& outline_locator,
    const coord_t prune_distance,
    const coord_t smooth_magnitude,
    const coord_t max_remove_colinear_dist) const
{
    auto tree_below = deepCopy();
    tree_below->prune(prune_distance);
    tree_below->straighten(smooth_magnitude, max_remove_colinear_dist);
    if (tree_below->realign(next_outlines, outline_locator, next_trees))
        next_trees.push_back(tree_below);
}

// NOTE: Depth-first, as currently implemented.
//       Skips the root (because that has no root itself), but all initial nodes will have the root point anyway.
void Node::visitBranches(const std::function<void(const Point&, const Point&)>& visitor) const
{
    for (const auto& node : m_children) {
        assert(node->m_parent.lock() == shared_from_this());
        visitor(m_p, node->m_p);
        node->visitBranches(visitor);
    }
}

// NOTE: Depth-first, as currently implemented.
void Node::visitNodes(const std::function<void(NodeSPtr)>& visitor)
{
    visitor(shared_from_this());
    for (const auto& node : m_children) {
        assert(node->m_parent.lock() == shared_from_this());
        node->visitNodes(visitor);
    }
}

Node::Node(const Point& p, const std::optional<Point>& last_grounding_location /*= std::nullopt*/) : 
    m_is_root(true), m_p(p), m_last_grounding_location(last_grounding_location)
{}

NodeSPtr Node::deepCopy() const
{
    NodeSPtr local_root = Node::create(m_p);
    local_root->m_is_root = m_is_root;
    if (m_is_root)
    {
        local_root->m_last_grounding_location = m_last_grounding_location.value_or(m_p);
    }
    local_root->m_children.reserve(m_children.size());
    for (const auto& node : m_children)
    {
        NodeSPtr child = node->deepCopy();
        child->m_parent = local_root;
        local_root->m_children.push_back(child);
    }
    return local_root;
}

void Node::reroot(const NodeSPtr &new_parent)
{
    if (! m_is_root) {
        auto old_parent = m_parent.lock();
        old_parent->reroot(shared_from_this());
        m_children.push_back(old_parent);
    }

    if (new_parent) {
        m_children.erase(std::remove(m_children.begin(), m_children.end(), new_parent), m_children.end());
        m_is_root = false;
        m_parent = new_parent;
    } else {
        m_is_root = true;
        m_parent.reset();
    }
}

NodeSPtr Node::closestNode(const Point& loc)
{
    NodeSPtr result = shared_from_this();
    auto closest_dist2 = coord_t((m_p - loc).cast<double>().norm());

    for (const auto& child : m_children) {
        NodeSPtr candidate_node = child->closestNode(loc);
        const auto child_dist2 = coord_t((candidate_node->m_p - loc).cast<double>().norm());
        if (child_dist2 < closest_dist2) {
            closest_dist2 = child_dist2;
            result = candidate_node;
        }
    }

    return result;
}

bool inside(const Polygons &polygons, const Point &p)
{
    int poly_count_inside = 0;
    for (const Polygon &poly : polygons) {
        const int is_inside_this_poly = Clipper3r::PointInPolygon(p, poly.points);
        if (is_inside_this_poly == -1)
            return true;
        poly_count_inside += is_inside_this_poly;
    }
    return (poly_count_inside % 2) == 1;
}

bool lineSegmentPolygonsIntersection(const Point& a, const Point& b, const EdgeGrid::Grid& outline_locator, Point& result, const coord_t within_max_dist)
{
    struct Visitor {
        bool operator()(coord_t iy, coord_t ix) {
            // Called with a row and colum of the grid cell, which is intersected by a line.
            auto cell_data_range = grid.cell_data_range(iy, ix);
            for (auto it_contour_and_segment = cell_data_range.first; it_contour_and_segment != cell_data_range.second; ++it_contour_and_segment) {
                // End points of the line segment and their vector.
                auto segment = grid.segment(*it_contour_and_segment);
                if (Vec2d ip; Geometry::segment_segment_intersection(segment.first.cast<double>(), segment.second.cast<double>(), this->line_a, this->line_b, ip))
                    if (double d = (this->intersection_pt - this->line_b).squaredNorm(); d < d2min) {
                        this->d2min = d;
                        this->intersection_pt = ip;
                    }
            }
            // Continue traversing the grid along the edge.
            return true;
        }

        const EdgeGrid::Grid& grid;
        Vec2d                 line_a;
        Vec2d                 line_b;
        Vec2d                 intersection_pt;
        double                d2min { std::numeric_limits<double>::max() };
    } visitor { outline_locator, a.cast<double>(), b.cast<double>() };

    outline_locator.visit_cells_intersecting_line(a, b, visitor);
    if (visitor.d2min < double(within_max_dist) * double(within_max_dist)) {
        result = Point(visitor.intersection_pt);
        return true;
    }
    return false;
}

bool Node::realign(const Polygons& outlines, const EdgeGrid::Grid& outline_locator, std::vector<NodeSPtr>& rerooted_parts)
{
    if (outlines.empty())
        return false;

    if (inside(outlines, m_p)) {
        // Only keep children that have an unbroken connection to here, realign will put the rest in rerooted parts due to recursion:
        Point coll;
        bool reground_me = false;
        m_children.erase(std::remove_if(m_children.begin(), m_children.end(), [&](const NodeSPtr &child) {
            bool connect_branch = child->realign(outlines, outline_locator, rerooted_parts);
            // Find an intersection of the line segment from p to child->p, at maximum outline_locator.resolution() * 2 distance from p.
            if (connect_branch && lineSegmentPolygonsIntersection(child->m_p, m_p, outline_locator, coll, outline_locator.resolution() * 2)) {
                child->m_last_grounding_location.reset();
                child->m_parent.reset();
                child->m_is_root = true;
                rerooted_parts.push_back(child);
                reground_me = true;
                connect_branch = false;
            }
            return ! connect_branch;
        }), m_children.end());
        if (reground_me)
            m_last_grounding_location.reset();
        return true;
    }

    // 'Lift' any decendants out of this tree:
    for (auto& child : m_children)
        if (child->realign(outlines, outline_locator, rerooted_parts)) {
            child->m_last_grounding_location = m_p;
            child->m_parent.reset();
            child->m_is_root = true;
            rerooted_parts.push_back(child);
        }

    m_children.clear();
    return false;
}

void Node::straighten(const coord_t magnitude, const coord_t max_remove_colinear_dist)
{
    straighten(magnitude, m_p, 0, int64_t(max_remove_colinear_dist) * int64_t(max_remove_colinear_dist));
}

Node::RectilinearJunction Node::straighten(
    const coord_t magnitude,
    const Point& junction_above,
    const coord_t accumulated_dist,
    const int64_t max_remove_colinear_dist2)
{
    constexpr coord_t junction_magnitude_factor_numerator = 3;
    constexpr coord_t junction_magnitude_factor_denominator = 4;

    const coord_t junction_magnitude = magnitude * junction_magnitude_factor_numerator / junction_magnitude_factor_denominator;
    if (m_children.size() == 1)
    {
        auto child_p = m_children.front();
        auto child_dist = coord_t((m_p - child_p->m_p).cast<double>().norm());
        RectilinearJunction junction_below = child_p->straighten(magnitude, junction_above, accumulated_dist + child_dist, max_remove_colinear_dist2);
        coord_t total_dist_to_junction_below = junction_below.total_recti_dist;
        const Point& a = junction_above;
        Point        b = junction_below.junction_loc;
        if (a != b) // should always be true!
        {
            Point ab = b - a;
            Point destination = (a.cast<int64_t>() + ab.cast<int64_t>() * int64_t(accumulated_dist) / std::max(int64_t(1), int64_t(total_dist_to_junction_below))).cast<coord_t>();
            if ((destination - m_p).cast<int64_t>().squaredNorm() <= int64_t(magnitude) * int64_t(magnitude))
                m_p = destination;
            else
                m_p += ((destination - m_p).cast<double>().normalized() * magnitude).cast<coord_t>();
        }
        { // remove nodes on linear segments
            constexpr coord_t close_enough = 10;

            child_p = m_children.front(); //recursive call to straighten might have removed the child
            const NodeSPtr& parent_node = m_parent.lock();
            if (parent_node &&
                (child_p->m_p - parent_node->m_p).cast<int64_t>().squaredNorm() < max_remove_colinear_dist2 &&
                Line::distance_to_squared(m_p, parent_node->m_p, child_p->m_p) < close_enough * close_enough) {
                child_p->m_parent = m_parent;
                for (auto& sibling : parent_node->m_children)
                { // find this node among siblings
                    if (sibling == shared_from_this())
                    {
                        sibling = child_p; // replace this node by child
                        break;
                    }
                }
            }
        }
        return junction_below;
    }
    else
    {
        constexpr coord_t weight = 1000;
        Point junction_moving_dir = ((junction_above - m_p).cast<double>().normalized() * weight).cast<coord_t>();
        bool prevent_junction_moving = false;
        for (auto& child_p : m_children)
        {
            const auto child_dist = coord_t((m_p - child_p->m_p).cast<double>().norm());
            RectilinearJunction below = child_p->straighten(magnitude, m_p, child_dist, max_remove_colinear_dist2);

            junction_moving_dir += ((below.junction_loc - m_p).cast<double>().normalized() * weight).cast<coord_t>();
            if (below.total_recti_dist < magnitude) // TODO: make configurable?
            {
                prevent_junction_moving = true; // prevent flipflopping in branches due to straightening and junctoin moving clashing
            }
        }
        if (junction_moving_dir != Point(0, 0) && ! m_children.empty() && ! m_is_root && ! prevent_junction_moving)
        {
            auto junction_moving_dir_len = coord_t(junction_moving_dir.norm());
            if (junction_moving_dir_len > junction_magnitude)
            {
                junction_moving_dir = junction_moving_dir * junction_magnitude / junction_moving_dir_len;
            }
            m_p += junction_moving_dir;
        }
        return RectilinearJunction{ accumulated_dist, m_p };
    }
}

// Prune the tree from the extremeties (leaf-nodes) until the pruning distance is reached.
coord_t Node::prune(const coord_t& pruning_distance)
{
    if (pruning_distance <= 0)
        return 0;

    coord_t max_distance_pruned = 0;
    for (auto child_it = m_children.begin(); child_it != m_children.end(); ) {
        auto& child = *child_it;
        coord_t dist_pruned_child = child->prune(pruning_distance);
        if (dist_pruned_child >= pruning_distance)
        { // pruning is finished for child; dont modify further
            max_distance_pruned = std::max(max_distance_pruned, dist_pruned_child);
            ++child_it;
        } else {
            const Point a = getLocation();
            const Point b = child->getLocation();
            const Point ba = a - b;
            const auto ab_len = coord_t(ba.cast<double>().norm());
            if (dist_pruned_child + ab_len <= pruning_distance) { 
                // we're still in the process of pruning
                assert(child->m_children.empty() && "when pruning away a node all it's children must already have been pruned away");
                max_distance_pruned = std::max(max_distance_pruned, dist_pruned_child + ab_len);
                child_it = m_children.erase(child_it);
            } else {
                // pruning stops in between this node and the child
                const Point n = b + (ba.cast<double>().normalized() * (pruning_distance - dist_pruned_child)).cast<coord_t>();
                assert(std::abs((n - b).cast<double>().norm() + dist_pruned_child - pruning_distance) < 10 && "total pruned distance must be equal to the pruning_distance");
                max_distance_pruned = std::max(max_distance_pruned, pruning_distance);
                child->setLocation(n);
                ++child_it;
            }
        }
    }

    return max_distance_pruned;
}

void Node::convertToPolylines(Polylines &output, const coord_t line_overlap) const
{
    Polylines result;
    result.emplace_back();
    convertToPolylines(0, result);
    removeJunctionOverlap(result, line_overlap);
    append(output, std::move(result));
}

void Node::convertToPolylines(size_t long_line_idx, Polylines &output) const
{
    if (m_children.empty()) {
        output[long_line_idx].points.push_back(m_p);
        return;
    }
    size_t first_child_idx = rand() % m_children.size();
    m_children[first_child_idx]->convertToPolylines(long_line_idx, output);
    output[long_line_idx].points.push_back(m_p);

    for (size_t idx_offset = 1; idx_offset < m_children.size(); idx_offset++) {
        size_t child_idx = (first_child_idx + idx_offset) % m_children.size();
        const Node& child = *m_children[child_idx];
        output.emplace_back();
        size_t child_line_idx = output.size() - 1;
        child.convertToPolylines(child_line_idx, output);
        output[child_line_idx].points.emplace_back(m_p);
    }
}

void Node::removeJunctionOverlap(Polylines &result_lines, const coord_t line_overlap) const
{
    const coord_t reduction    = line_overlap;
    size_t        res_line_idx = 0;
    while (res_line_idx < result_lines.size()) {
        Polyline &polyline = result_lines[res_line_idx];
        if (polyline.size() <= 1) {
            polyline = std::move(result_lines.back());
            result_lines.pop_back();
            continue;
        }

        coord_t to_be_reduced = reduction;
        Point a = polyline.back();
        for (int point_idx = int(polyline.size()) - 2; point_idx >= 0; point_idx--) {
            const Point b = polyline.points[point_idx];
            const Point ab = b - a;
            const auto ab_len = coord_t(ab.cast<double>().norm());
            if (ab_len >= to_be_reduced) {
                polyline.points.back() = a + (ab.cast<double>() * (double(to_be_reduced) / ab_len)).cast<coord_t>();
                break;
            } else {
                to_be_reduced -= ab_len;
                polyline.points.pop_back();
            }
            a = b;
        }

        if (polyline.size() <= 1) {
            polyline = std::move(result_lines.back());
            result_lines.pop_back();
        } else
            ++ res_line_idx;
    }
}

#ifdef LIGHTNING_TREE_NODE_DEBUG_OUTPUT
void export_to_svg(const NodeSPtr &root_node, SVG &svg)
{
    for (const NodeSPtr &children : root_node->m_children) {
        svg.draw(Line(root_node->getLocation(), children->getLocation()), "red");
        export_to_svg(children, svg);
    }
}

void export_to_svg(const std::string &path, const Polygons &contour, const std::vector<NodeSPtr> &root_nodes) {
    BoundingBox bbox = get_extents(contour);

    bbox.offset(SCALED_EPSILON);
    SVG svg(path, bbox);
    svg.draw_outline(contour, "blue");

    for (const NodeSPtr &root_node: root_nodes) {
        for (const NodeSPtr &children: root_node->m_children) {
            svg.draw(Line(root_node->getLocation(), children->getLocation()), "red");
            export_to_svg(children, svg);
        }
    }
}
#endif /* LIGHTNING_TREE_NODE_DEBUG_OUTPUT */

} // namespace Slic3r::FillLightning
