extends RefCounted
class_name VizSimpleLeg

var _error : String = ""
var _begin : VisualizationSpot
var _end : VisualizationSpot
var _width : float
var _bend_lip : float
var _bend : VizBend

const MIN_SEGMENT_LENGTH=0.01
const EPSILON=0.00001

func _init(p_begin : VisualizationSpot, p_end : VisualizationSpot, p_width : float, p_bend_lip : float):
	_begin = p_begin
	_end = p_end
	_width = p_width
	_bend_lip = p_bend_lip
	if not _has_valid_normal():
		return
	if _is_straight():
		return
	_bend = VizBend.new(_begin, _end, p_width, p_bend_lip)
	if not _bend.is_invalid():
		_check_offsets(_begin, _end)

# ---------------------------------------------
# Public Methods

func get_error() -> String:
	if _error != "":
		return _error
	if _bend != null:
		return _bend.get_error()
	return ""

func is_invalid() -> bool:
	if _error != "":
		return true
	if _bend != null:
		return _bend.is_invalid()
	return false

func extend_mesh(mesh_node : Node3D, u : float, left : Vector3, right : Vector3, segs : int, sharpness : float, mat : Material) -> float:
	if _bend == null:
		u = _extend_straight_leg(mesh_node, u, left, right, _begin.point, _end.point, _begin.normal, mat)
	else:
		u = _extend_straight_leg(mesh_node, u, left, right, _begin.point, _bend.get_begin(), _begin.normal, mat)
		if _bend.is_angled():
			u = _create_begin_triangle(mesh_node, u, _begin.normal, _bend.get_begin_triangle(), mat)
		u = _create_bend(mesh_node, u, segs, sharpness, mat)
		if _bend.is_angled():
			u = _create_end_triangle(mesh_node, u, _end.normal, _bend.get_end_triangle(), mat)
		u = _create_straight_leg(mesh_node, u, _bend.get_end(), _end.point, _end.normal, mat)
	return u

func update_mesh(mesh_node : Node3D, u : float, segs : int, sharpness : float, mat : Material) -> float:
	if _bend == null:
		u = _create_straight_leg(mesh_node, u, _begin.point, _end.point, _begin.normal, mat)
	else:
		u = _create_straight_leg(mesh_node, u, _begin.point, _bend.get_begin(), _begin.normal, mat)
		if _bend.is_angled():
			u = _create_begin_triangle(mesh_node, u, _begin.normal, _bend.get_begin_triangle(), mat)
		u = _create_bend(mesh_node, u, segs, sharpness, mat)
		if _bend.is_angled():
			u = _create_end_triangle(mesh_node, u, _end.normal, _bend.get_end_triangle(), mat)
		u = _create_straight_leg(mesh_node, u, _bend.get_end(), _end.point, _end.normal, mat)
	return u

func get_begin() -> VisualizationSpot:
	return _begin

func get_leg_bend_begin() -> Vector3:
	if _bend == null:
		return (_end.point - _begin.point)/2.0
	return _bend.get_begin_triangle().get_leg_mid()

func get_begin_ray() -> Vector3:
	if _bend == null:
		return (_end.point - _begin.point).normalized()
	return (_bend.get_begin() - _begin.point).normalized()

func adjust_begin(distance : float):
	var new_begin := VisualizationSpot.new()
	new_begin.point = _begin.point + distance * get_begin_ray()
	new_begin.normal = _begin.normal
	if _check_offsets(new_begin, _end):
		_begin = new_begin

func get_end() -> VisualizationSpot:
	return _end
	
func get_leg_bend_end() -> Vector3:
	if _bend == null:
		return (_end.point - _begin.point)/2.0
	return _bend.get_end_triangle().get_leg_mid()
	
func get_end_left() -> Vector3:
	var segment := _end.point - get_leg_bend_end()
	var binormal := segment.cross(_end.normal).normalized()
	var half_width = binormal*_width/2.0
	return _end.point - half_width

func get_end_right() -> Vector3:
	var segment := _end.point - get_leg_bend_end()
	var binormal := segment.cross(_end.normal).normalized()
	var half_width = binormal*_width/2.0
	return _end.point + half_width

func get_end_ray() -> Vector3:
	if _bend == null:
		return (_end.point - _begin.point).normalized()
	return (_end.point - _bend.get_end()).normalized()

func adjust_end(distance : float):
	var new_end := VisualizationSpot.new()
	new_end.point = _end.point - distance * get_end_ray()
	new_end.normal = _end.normal
	if _check_offsets(_begin, new_end):
		_end = new_end

# ---------------------------------------------
# Local Methods

func _check_offsets(p_begin : VisualizationSpot, p_end : VisualizationSpot) -> bool:
	var dir := (_end.point - _begin.point).normalized()
	var min_len := MIN_SEGMENT_LENGTH + EPSILON
	var max_begin := _end.point - dir * min_len
	var max_end := _begin.point + dir * min_len
	if _bend != null:
		max_begin = _bend.get_begin() - dir * min_len
		max_end = _bend.get_end() + dir * min_len
	var new_begin_dir := (max_begin - p_begin.point).normalized()
	var new_end_dir := (p_end.point - max_end).normalized()
	if new_begin_dir.dot(dir) <= 0.0:
		_error = "bend start %s is too near %s to be bent with this width and lip" % [ _begin.point, _end.point ]
		return false
	if new_end_dir.dot(dir) <= 0.0:
		_error = "bend end %s is too near %s to be bent with this width and lip" % [ _end.point, _begin.point ]
		return false
	return true

func _has_valid_normal() -> bool:
	var segment = _end.point - _begin.point
	var binormal = segment.cross(_begin.normal).normalized()
	if not binormal.is_normalized():
		_error = "%s to %s is parallel to normal %s" % [ _begin.point, _end.point, _begin.normal ]
		return false
	return true

func _is_straight() -> bool:
	if _begin.normal.is_equal_approx(_end.normal):
		var segment = _end.point - _begin.point
		if is_equal_approx(segment.dot(_begin.normal), 0.0):
			return true
		_error = "%s to %s have same normal but are not in same plane" % [ _begin.point, _end.point ]
	return false

func _extend_straight_leg(mesh_node : Node3D, u : float, left : Vector3, right : Vector3, begin : Vector3, end : Vector3, normal : Vector3, mat : Material):
	var segment := end - begin
	var binormal := segment.cross(normal).normalized()
	var half_width = binormal*_width/2.0
	var seg := begin - end
	var seg_len := seg.length()

	var st := SurfaceTool.new()
	st.begin(Mesh.PRIMITIVE_TRIANGLE_STRIP)
	st.set_color(Color(1, 0, 0))
	st.set_normal(normal)

	st.set_uv(Vector2(u, 1))
	st.add_vertex(right)

	st.set_uv(Vector2(u, 0))
	st.add_vertex(left)

	st.set_uv(Vector2(u+seg_len, 1))
	st.add_vertex(end+half_width)

	st.set_uv(Vector2(u+seg_len, 0))
	st.add_vertex(end-half_width)

	st.set_material(mat)
	mesh_node.mesh = st.commit(mesh_node.mesh)

	return u + seg_len

func _create_straight_leg(mesh_node : Node3D, u : float, begin : Vector3, end : Vector3, normal : Vector3, mat : Material):
	var segment := end - begin
	var binormal := segment.cross(normal).normalized()
	var half_width = binormal*_width/2.0
	var seg := begin - end
	var seg_len := seg.length()

	var st := SurfaceTool.new()
	st.begin(Mesh.PRIMITIVE_TRIANGLE_STRIP)
	st.set_color(Color(1, 0, 0))
	st.set_normal(normal)

	st.set_uv(Vector2(u, 1))
	st.add_vertex(begin+half_width)

	st.set_uv(Vector2(u, 0))
	st.add_vertex(begin-half_width)

	st.set_uv(Vector2(u+seg_len, 1))
	st.add_vertex(end+half_width)

	st.set_uv(Vector2(u+seg_len, 0))
	st.add_vertex(end-half_width)

	st.set_material(mat)
	mesh_node.mesh = st.commit(mesh_node.mesh)

	return u + seg_len

func _create_begin_triangle(mesh_node : Node3D, u : float, normal : Vector3, triangle : VizTriangle, mat : Material):
	var st := SurfaceTool.new()
	st.begin(Mesh.PRIMITIVE_TRIANGLES)
	st.set_color(Color(1, 0, 0))
	st.set_normal(normal)

	st.set_uv(Vector2(u, 0))
	st.add_vertex(triangle.leg_left)
	st.set_uv(Vector2(u + triangle.distance, triangle.get_fwd_side()))
	st.add_vertex(triangle.leg_fwd)
	st.set_uv(Vector2(u, 1))
	st.add_vertex(triangle.leg_right)
	st.set_material(mat)
	mesh_node.mesh = st.commit(mesh_node.mesh)
	return u + triangle.distance

func _create_end_triangle(mesh_node : Node3D, u : float, normal : Vector3, triangle : VizTriangle, mat : Material):
	var st := SurfaceTool.new()
	st.begin(Mesh.PRIMITIVE_TRIANGLES)
	st.set_color(Color(1, 0, 0))
	st.set_normal(normal)

	st.set_uv(Vector2(u + triangle.distance, 0))
	st.add_vertex(triangle.leg_left)
	st.set_uv(Vector2(u + triangle.distance, 1))
	st.add_vertex(triangle.leg_right)
	st.set_uv(Vector2(u, triangle.get_fwd_side()))
	st.add_vertex(triangle.leg_fwd)
	st.set_material(mat)
	mesh_node.mesh = st.commit(mesh_node.mesh)
	return u + triangle.distance

func _create_bend(mesh_node : Node3D, u : float, segs : int, sharpness : float, mat : Material):
	var right_u := u - _bend.get_begin_triangle().distance
	var left_u := u
	if _bend.is_angled_against_curve():
		right_u = u
		left_u = u - _bend.get_begin_triangle().distance
	var arc_len := 0.0

	var segment := _bend.get_begin() - _begin.point 
	var binormal := segment.cross(_begin.normal).normalized()
	var half_width = _bend.get_half_bend_width()
	var half_width_normal := half_width.normalized()
	var st := SurfaceTool.new()
	st.begin(Mesh.PRIMITIVE_TRIANGLE_STRIP)
	st.set_normal(_begin.normal)
	st.set_uv(Vector2(right_u, 1))
	st.add_vertex(_bend.get_begin_triangle().bend_right)
	st.set_uv(Vector2(left_u, 0))
	st.add_vertex(_bend.get_begin_triangle().bend_left)
	var shift_width := _bend.get_shift_width()
	var last_point := _bend.get_arc_begin_point()
	var total_len := 0.0
	for i in range(0, segs):
		var weight := float(i) / float(segs-1)
		var cur_point := _bend.get_arc_point(weight, sharpness)
		total_len += cur_point.distance_to(last_point)
		last_point = cur_point
	total_len += _bend.get_arc_end_point().distance_to(last_point)
	last_point = _bend.get_arc_begin_point()
	for i in range(1, segs-1):
		var weight := float(i) / float(segs-1)
		var cur_point := _bend.get_arc_point(weight, sharpness)
		arc_len += cur_point.distance_to(last_point)
		var shift := shift_width * (arc_len / total_len)
		var pos := cur_point + shift - _bend.get_shift_offset()
		var cur_normal := Vector3(cur_point - last_point).normalized().cross(half_width_normal)
		last_point = cur_point
		st.set_normal(cur_normal)
		st.set_uv(Vector2(right_u + arc_len, 1))
		st.add_vertex(pos-half_width)
		st.set_uv(Vector2(left_u + arc_len, 0))
		st.add_vertex(pos+half_width)
	st.set_normal(_end.normal)
	st.set_uv(Vector2(right_u + total_len, 1))
	st.add_vertex(_bend.get_end_triangle().bend_right)
	st.set_uv(Vector2(left_u + total_len, 0))
	st.add_vertex(_bend.get_end_triangle().bend_left)
	st.set_material(mat)
	mesh_node.mesh = st.commit(mesh_node.mesh)
	u = right_u + total_len
	if _bend.is_angled_against_curve():
		u = left_u + total_len
	return u

func _to_string():
	if _bend != null:
		return "SLEG(%s->%s through %s)" % [ _begin, _end, _bend ]
	return "SLEG(%s->%s)" % [ _begin, _end ]

