extends Node
class_name Visibility2D

# Based on: https://www.redblobgames.com/articles/visibility/Visibility.hx

# Limitations:
# - Segments cant intersect each other, splitting is required for such cases.

# todo: Make it extend plain object, handle lifetime manually.
class EndPoint:
    var point: Vector2
    var begin: bool
    var segment: int
    var angle: float

    static func sort(p_a: EndPoint, p_b: EndPoint) -> bool:
        if p_a.angle > p_b.angle: return true
        elif p_a.angle < p_b.angle: return false
        elif not p_a.begin and p_b.begin: return true
        else: return false

var _endpoints: Array # of EndPoint
var _sorted_endpoints: Array # of EndPoint
var _open: PoolIntArray # of Segment indices

var center: Vector2
var output: PoolVector2Array

# todo: Ability to cache builder state for static geometry.
class Builder:
    var target

    func view_point(p_point: Vector2) -> Builder:
        target.center = p_point
        return self

    # todo: Use it to cull out endpoints out of working region.
    func bounds(p_area: Rect2) -> Builder:
        target._add_segment(p_area.position, Vector2(p_area.end.x, p_area.position.y))
        target._add_segment(Vector2(p_area.end.x, p_area.position.y), p_area.end)
        target._add_segment(p_area.end, Vector2(p_area.position.x, p_area.end.y))
        target._add_segment(Vector2(p_area.position.x, p_area.end.y), p_area.position)
        return self

    func line(p_line: Line2D) -> Builder:
        for i in range(0, p_line.points.size() - 1):
            target._add_segment(p_line.position + p_line.points[i],
                p_line.position + p_line.points[i + 1])
        return self

    func polygon(p_polygon: Polygon2D) -> Builder:
        var points := p_polygon.polygon
        for i in range(0, points.size() - 1):
            target._add_segment(p_polygon.position + points[i],
                p_polygon.position + points[i + 1])
        target._add_segment(p_polygon.position + points[points.size() - 1],
            p_polygon.position + points[0])
        return self

    func occluder(p_object: Object) -> Builder:
        if p_object is Line2D:
            return line(p_object)
        elif p_object is Polygon2D:
            return polygon(p_object)
        else:
            push_error("Unknown occluder type")
            return self

    func finalize():
        target._finalize()

func _add_segment(p_point0: Vector2, p_point1: Vector2):
    var point0 := EndPoint.new()
    var point1 := EndPoint.new()
    point0.segment = _endpoints.size()
    point1.segment = _endpoints.size()
    point0.point = p_point0
    point1.point = p_point1
    _endpoints.append(point0)
    _endpoints.append(point1)

func init_builder() -> Builder:
    # todo: Reuse
    _endpoints.resize(0)
    var result := Builder.new()
    result.target = self
    return result

func _finalize():
    # todo: Only needs to be done when endpoints or center is changed.
    for segment in range(0, _endpoints.size(), 2):
        var p1 := _endpoints[segment] as EndPoint
        var p2 := _endpoints[segment + 1] as EndPoint
        p1.angle = (p1.point - center).angle()
        p2.angle = (p2.point - center).angle()
        # todo: Simplify to one expression.
        var da := p2.angle - p1.angle
        if da <= PI: da += TAU
        if da > PI: da -= TAU
        p1.begin = da > 0.0
        p2.begin = not p1.begin

func _is_segment_in_front(p_segment1: int, p_segment2: int) -> bool:
    var s1p1 := _endpoints[p_segment1].point as Vector2
    var s1p2 := _endpoints[p_segment1 + 1].point as Vector2
    var s2p1 := _endpoints[p_segment2].point as Vector2
    var s2p2 := _endpoints[p_segment2 + 1].point as Vector2

    # todo: Can we use something simpler than interpolation?
    var d := s1p2 - s1p1
    var p := s2p1.linear_interpolate(s2p2, 0.01)
    var a1 := (d.x * (p.y - s1p1.y) \
             - d.y * (p.x - s1p1.x)) < 0.0
    p = s2p2.linear_interpolate(s2p1, 0.01)
    var a2 := (d.x * (p.y - s1p1.y) \
             - d.y * (p.x - s1p1.x)) < 0.0
    var a3 := (d.x * (center.y - s1p1.y) \
             - d.y * (center.x - s1p1.x)) < 0.0

    if a1 == a2 and a2 == a3: return true

    d = s2p2 - s2p1
    p = s1p1.linear_interpolate(s1p2, 0.01)
    var b1 := (d.x * (p.y - s2p1.y) \
             - d.y * (p.x - s2p1.x)) < 0.0
    p = s1p2.linear_interpolate(s1p1, 0.01)
    var b2 := (d.x * (p.y - s2p1.y) \
             - d.y * (p.x - s2p1.x)) < 0.0
    var b3 := (d.x * (center.y - s2p1.y) \
             - d.y * (center.x - s2p1.x)) < 0.0

    return b1 == b2 and b2 != b3

func sweep() -> PoolVector2Array:
    output.resize(0)
    # todo: Only duplicate and sort on change.
    _sorted_endpoints = _endpoints.duplicate()
    _sorted_endpoints.sort_custom(EndPoint, "sort")

    var start_angle := 0.0

    # todo: Inline passes.
    for n_pass in range(2):
        for p_idx in range(_sorted_endpoints.size() - 1, -1, -1):
            var p := _sorted_endpoints[p_idx] as EndPoint
            var old := -1 if _open.empty() else _open[0]

            if p.begin:
                var idx := 0
                while idx < _open.size() and _is_segment_in_front(p.segment, _open[idx]):
                    idx += 1
                # warning-ignore:return_value_discarded
                _open.insert(idx, p.segment)
            else:
                var idx := _open.rfind(p.segment)
                if idx != -1: _open.remove(idx)
                # todo: Second pass can assume that it will be found.
                # _open.remove(_open.rfind(p.segment))

            if old != (-1 if _open.empty() else _open[0]):
                if n_pass == 1:
                    # todo: Distance should be configurable.
                    var p3 := _endpoints[old].point as Vector2 if old != -1 else \
                        center + Vector2(cos(start_angle), sin(start_angle)) * 500.0
                    var t2 := Vector2(cos(p.angle), sin(p.angle))
                    var p4 := p3.direction_to(_endpoints[old + 1].point) if old != -1 else t2 

                    var l = Geometry.line_intersects_line_2d(p3, p4, center,
                        Vector2(cos(start_angle), sin(start_angle)))
                    if l != null: output.append(l)
                    l = Geometry.line_intersects_line_2d(p3, p4, center, t2)
                    if l != null: output.append(l)

                start_angle = p.angle

    _open.resize(0)

    return output
