--math for 2D elliptic arcs defined as:
--  (center_x, center_y, radius_x, radius_y, start_angle, sweep_angle, [rotation], [x2, y2]).
--angles are expressed in degrees, not radians.
--sweep angle is capped between -360..360deg.
--x2, y2 is an optional override of arc's second end point to use when numerical exactness of the endpoint is required.
--mt is an affine transform that applies to the resulted segments.
--segment_max_sweep is for limiting the arc portion that each bezier segment can cover and is computed automatically.

if not ... then require'path2d_arc_demo' end

local reflect_point = require'path2d_point'.reflect_point
local rotate_point  = require'path2d_point'.rotate_point
local hypot = require'path2d_point'.hypot
local line_to_bezier3 = require'path2d_line'.to_bezier3
local bezier3 = require'path2d_bezier3'
local point_around = require'path2d_point'.point_around
local point_angle = require'path2d_point'.point_angle
local distance2 = require'path2d_point'.distance2
local bezier3_length = require'path2d_bezier3'.length
local bezier3_bounding_box = require'path2d_bezier3'.bounding_box

local abs, min, max, sqrt, ceil, sin, cos, radians =
	math.abs, math.min, math.max, math.sqrt, math.ceil, math.sin, math.cos, math.rad

local angle_epsilon = 1e-10

--observed sweep: an arc's sweep can be larger than 360deg but we can only render the first -360..360deg of it.
local function observed_sweep(sweep_angle)
	return max(min(sweep_angle, 360), -360)
end

--observed sweep between two arbitrary angles, sweeping from a1 to a2 in a specified direction.
local function sweep_between(a1, a2, clockwise)
	a1 = a1 % 360
	a2 = a2 % 360
	clockwise = clockwise ~= false
	local sweep = a2 - a1
	if sweep < 0 and clockwise then
		sweep = sweep + 360
	elseif sweep > 0 and not clockwise then
		sweep = sweep - 360
	end
	return sweep
end

--angle time on an arc (or outside the arc if outside 0..1 range) for a specified angle.
local function sweep_time(hit_angle, start_angle, sweep_angle)
	sweep_angle = observed_sweep(sweep_angle)
	return sweep_between(start_angle, hit_angle, sweep_angle >= 0) / sweep_angle
end

--check if an angle is inside the sweeped arc.
local function is_sweeped(hit_angle, start_angle, sweep_angle)
	local t = sweep_time(hit_angle, start_angle, sweep_angle)
	return t >= 0 and t <= 1
end

--evaluate ellipse at angle a.
local function point_at(a, cx, cy, rx, ry, rotation, mt)
	rx, ry = abs(rx), abs(ry)
	a = radians(a)
	rotation = rotation or 0
	local x = cx + cos(a) * rx
	local y = cy + sin(a) * ry
	if rotation ~= 0 then
		x, y = rotate_point(x, y, cx, cy, rotation)
	end
	if mt then
		x, y = mt(x, y)
	end
	return x, y
end

--evaluate elliptic arc at time t (the time between 0..1 covers the arc over the sweep interval).
local function point(t, cx, cy, rx, ry, start_angle, sweep_angle, rotation, x2, y2, mt)
	return point_at(start_angle + t * observed_sweep(sweep_angle), cx, cy, rx, ry, rotation, mt)
end

--tangent vector on elliptic arc at time t based on http://content.gpwiki.org/index.php/Tangents_To_Circles_And_Ellipses.
--the vector is always oriented towards the sweep of the arc.
local function tangent_vector(t, cx, cy, rx, ry, start_angle, sweep_angle, rotation, x2, y2, mt)
	rx, ry = abs(rx), abs(ry)
	sweep_angle = observed_sweep(sweep_angle)
	rotation = rotation or 0
	local a = radians(start_angle + t * sweep_angle)
	--px,py is the point at time t on the origin-centered, unrotated ellipse (0, 0, rx, ry, 0).
	local px = cos(a) * rx
	local py = sin(a) * ry
	--tx,ty is the tip point of the tangent vector at angle a, oriented towards the sweep.
	local sign = sweep_angle >= 0 and 1 or -1
	local tx = px + sign * rx * py / ry
	local ty = py - sign * ry * px / rx
	--now rotate, translate to origin, and transform the points as needed.
	if rotation ~= 0 then
		px, py = rotate_point(px, py, 0, 0, rotation)
		tx, ty = rotate_point(tx, ty, 0, 0, rotation)
	end
	px, py = cx + px, cy + py
	tx, ty = cx + tx, cy + ty
	if t == 1 and x2 then
		px, py = x2, y2
	end
	if mt then
		px, py = mt(px, py)
		tx, ty = mt(tx, ty)
	end
	return px, py, tx, ty
end

local function endpoints(cx, cy, rx, ry, start_angle, sweep_angle, rotation, x2, y2, mt)
	local x1, y1 = point_at(start_angle, cx, cy, rx, ry, rotation, mt)
	if not x2 then
		x2, y2 = point_at(start_angle + observed_sweep(sweep_angle), cx, cy, rx, ry, rotation, mt)
	elseif mt then
		x2, y2 = mt(x2, y2)
	end
	return x1, y1, x2, y2
end

--determine the length of the major axis of a circle of the given radius after applying an affine transformation.
--look at cairo-matrix.c for the math behind it.
local function transformed_circle_major_axis(mt, r)
	if not mt or mt:has_unity_scale() then return r end
	local a, b, c, d = mt:unpack()
	local i = a^2 + b^2
	local j = c^2 + d^2
	local f = (i + j) / 2
	local g = (i - j) / 2
	local h = a*c + b*d
	return r * sqrt(f + hypot(g, h))
end

--this formula is such that enables a non-oscillating segment-time-to-arc-time at screen resolutions (see demo).
local function best_segment_max_sweep(mt, rx, ry)
	local scale_factor = transformed_circle_major_axis(mt, max(abs(rx), abs(ry))) / 1024
	scale_factor = max(scale_factor, 0.1) --cap scale factor so that we don't create sweeps larger than 90 deg.
	return sqrt(1/scale_factor^0.6) * 30 --faster way to say 1/2^log10(scale) * 30
end

local function segment(cx, cy, rx, ry, start_angle, sweep_angle)
	local a = radians(sweep_angle / 2)
	local x0 = cos(a)
	local y0 = sin(a)
	local tx = (1 - x0) * 4 / 3
	local ty = y0 - tx * x0 / y0
	local px1 =  x0 + tx
	local py1 = -ty
	local px2 =  x0 + tx
	local py2 =  ty
	local px3 =  x0
	local py3 =  y0
	local a = radians(start_angle + sweep_angle / 2)
	local sn = sin(a)
	local cs = cos(a)
	return
		cx + rx * (px1 * cs - py1 * sn), --c1x
		cy + ry * (px1 * sn + py1 * cs), --c1y
		cx + rx * (px2 * cs - py2 * sn), --c2x
		cy + ry * (px2 * sn + py2 * cs), --c2y
		cx + rx * (px3 * cs - py3 * sn), --p2x
		cy + ry * (px3 * sn + py3 * cs)  --p2y
end

local function to_bezier3(write, cx, cy, rx, ry, start_angle, sweep_angle, rotation, x2, y2, mt, segment_max_sweep)
	if abs(sweep_angle) < angle_epsilon then
		local x1, y1, x2, y2 = endpoints(cx, cy, rx, ry, start_angle, sweep_angle, rotation, x2, y2, mt)
		write('curve', select(3, line_to_bezier3(x1, y1, x2, y2)))
	end

	rx, ry = abs(rx), abs(ry)
	sweep_angle = observed_sweep(sweep_angle)
	rotation = rotation or 0
	if not x2 then
		x2, y2 = point_at(start_angle + sweep_angle, cx, cy, rx, ry, rotation, mt)
	elseif mt then
		x2, y2 = mt(x2, y2)
	end
	segment_max_sweep = segment_max_sweep or best_segment_max_sweep(mt, rx, ry)

	local segments = ceil(abs(sweep_angle / segment_max_sweep))
	local segment_sweep = sweep_angle / segments
	local end_angle = start_angle + sweep_angle - segment_sweep / 2

	for angle = start_angle, end_angle, segment_sweep do
		local bx2, by2, bx3, by3, bx4, by4 = segment(cx, cy, rx, ry, angle, segment_sweep)
		if rotation ~= 0 then
			bx2, by2 = rotate_point(bx2, by2, cx, cy, rotation)
			bx3, by3 = rotate_point(bx3, by3, cx, cy, rotation)
			bx4, by4 = rotate_point(bx4, by4, cx, cy, rotation)
		end
		if mt then
			bx2, by2 = mt(bx2, by2)
			bx3, by3 = mt(bx3, by3)
			bx4, by4 = mt(bx4, by4)
		end
		if abs(end_angle - angle) < abs(segment_sweep) then --last segment: override endpoint with the specified one
			bx4, by4 = x2, y2
		end
		write('curve', bx2, by2, bx3, by3, bx4, by4)
	end
end

--given the time t on the i'th arc segment of an arc, return the corresponding arc time.
--we assume that time found on the bezier segment approximates well the time on the arc segment.
--the assumption is only accurate if the arc is composed of enough segments, given arc's transformed size.
local function segment_time_to_arc_time(i, t, sweep_angle, segment_max_sweep)
	local sweep_angle = abs(observed_sweep(sweep_angle))
	if sweep_angle < angle_epsilon then
		return t
	end
	local segments = ceil(sweep_angle / segment_max_sweep)
	return (i-1+t) / segments
end

--given the time t on an arc return the corresponding segment number and the time on that segment.
local function arc_time_to_segment_time(t, sweep_angle, segment_max_sweep)
	local sweep_angle = abs(observed_sweep(sweep_angle))
	if sweep_angle < angle_epsilon then
		return 1, t
	end
	local segments = ceil(sweep_angle / segment_max_sweep)
	local d = t * segments
	local i = math.floor(d)
	return i+1, d-i
end

--shortest distance-squared from point (x0, y0) to a circular arc, plus the touch point, and the time in the arc
--where the touch point splits the arc.
local function circular_arc_hit(x0, y0, cx, cy, r, start_angle, sweep_angle, x2, y2)
	r = abs(r)
	sweep_angle = observed_sweep(sweep_angle)
	if x0 == cx and y0 == cy then --projecting from the center
		local x, y = point_around(cx, cy, r, start_angle)
		return r, x, y, 0
	end
	local hit_angle = point_angle(x0, y0, cx, cy)
	local end_angle = start_angle + observed_sweep(sweep_angle)
	local t = sweep_time(hit_angle, start_angle, sweep_angle)
	if t < 0 or t > 1 then --hit point is outside arc's sweep opening, check distance to end points
		local x1, y1, x2, y2 = endpoints(cx, cy, r, r, start_angle, sweep_angle, x2, y2)
		local d1 = distance2(x0, y0, x1, y1)
		local d2 = distance2(x0, y0, x2, y2)
		if d1 <= d2 then
			return d1, x1, y1, 0
		else
			return d2, x2, y2, sweep_time(end_angle, start_angle, sweep_angle)
		end
	end
	local x, y = point_around(cx, cy, r, hit_angle)
	return distance2(x0, y0, x, y), x, y, t
end

--arc hit under affine transform: we construct the arc from a number of bezier segments, hit those and then
--compute the arc time from segment time. segment_max_sweep must be small enough given arc's size.
local function hit(x0, y0, cx, cy, rx, ry, start_angle, sweep_angle, rotation, x2, y2, mt, segment_max_sweep)
	if rx == ry and not mt then
		return circular_arc_hit(x0, y0, cx, cy, rx, start_angle, sweep_angle, x2, y2)
	end
	local i = 0 --segment count
	local mind, minx, miny, mint, mini
	local x1, y1 = endpoints(cx, cy, rx, ry, start_angle, sweep_angle, rotation, x2, y2, mt)
	local function write(s, ...)
		i = i + 1
		local d, x, y, t = bezier3.hit(x0, y0, x1, y1, ...)
		x1, y1 = select(5, ...)
		if not mind or d < mind then
			mind, minx, miny, mint, mini = d, x, y, t, i
		end
	end
	segment_max_sweep = segment_max_sweep or best_segment_max_sweep(mt, rx, ry)
	to_bezier3(write, cx, cy, rx, ry, start_angle, sweep_angle, rotation, x2, y2, mt, segment_max_sweep)
	mint = segment_time_to_arc_time(mini, mint, sweep_angle, segment_max_sweep)
	return mind, minx, miny, mint
end

local function split(t, cx, cy, rx, ry, start_angle, sweep_angle, rotation, x2, y2)
	t = min(max(t,0),1)
	sweep_angle = observed_sweep(sweep_angle)
	local sweep1 = t * sweep_angle
	local sweep2 = sweep_angle - sweep1
	local split_angle = start_angle + sweep1
	return
		cx, cy, rx, ry, start_angle, sweep1, rotation,        --first arc
		cx, cy, rx, ry, split_angle, sweep2, rotation, x2, y2 --second arc
end

--length of circular arc at time t.
local function circular_arc_length(t, cx, cy, r, start_angle, sweep_angle)
	return abs(t * radians(sweep_angle) * r)
end

local function length(t, cx, cy, rx, ry, start_angle, sweep_angle, rotation, x2, y2, mt, segment_max_sweep)
	if rx == ry and not mt then
		return circular_arc_length(t, cx, cy, rx, start_angle, sweep_angle)
	else
		--decompose and compute the sum of the lengths of the segments
		segment_max_sweep = segment_max_sweep or best_segment_max_sweep(mt, rx, ry)
		local maxseg, segt = arc_time_to_segment_time(t, sweep_angle, segment_max_sweep)
		local x1, y1 = endpoints(cx, cy, rx, ry, start_angle, sweep_angle, rotation, x2, y2, mt)
		local cseg, len = 0, 0
		local function write(_, x2, y2, x3, y3, x4, y4)
			cseg = cseg + 1
			if cseg > maxseg or (cseg == maxseg and segt == 0) then return end
			len = len + bezier3_length(cseg < maxseg and 1 or segt, x1, y1, x2, y2, x3, y3, x4, y4)
			x1, y1 = x4, y4
		end
		to_bezier3(write, cx, cy, rx, ry, start_angle, sweep_angle, rotation, x2, y2, mt, segment_max_sweep)
		return len
	end
end

--bounding box as (x,y,w,h) for a circular arc.
local function circular_arc_bounding_box(cx, cy, r, start_angle, sweep_angle, x2, y2)
	local x1, y1, x2, y2 = endpoints(cx, cy, r, r, start_angle, sweep_angle, 0, x2, y2)
	--assume the bounding box is between endpoints, i.e. that the arc doesn't touch any of its circle's extremities.
	local x1, x2 = min(x1, x2), max(x1, x2)
	local y1, y2 = min(y1, y2), max(y1, y2)
	if is_sweeped(0, start_angle, sweep_angle) then --arc touches its circle's rightmost point
		x2 = cx + r
	end
	if is_sweeped(90, start_angle, sweep_angle) then --arc touches its circle's most bottom point
		y2 = cy + r
	end
	if is_sweeped(180, start_angle, sweep_angle) then --arc touches its circle's leftmost point
		x1 = cx - r
	end
	if is_sweeped(-90, start_angle, sweep_angle) then --arc touches its circle's topmost point
		y1 = cy - r
	end
	return x1, y1, x2-x1, y2-y1
end

--grow a bounding box with another bounding box
local function grow_bbox(bx, by, bw, bh, x, y, w, h)
	local ax1, ay1, ax2, ay2 = x, y, x+w, y+h
	local bx1, by1, bx2, by2 = bx, by, bx+bw, by+bh
	bx1 = min(bx1, ax1, ax2)
	by1 = min(by1, ay1, ay2)
	bx2 = max(bx2, ax1, ax2)
	by2 = max(by2, ay1, ay2)
	return bx1, by1, bx2-bx1, by2-by1
end

local function bounding_box(cx, cy, rx, ry, start_angle, sweep_angle, rotation, x2, y2, mt, segment_max_sweep)
	if rx == ry and not mt then --TODO: or mt:straight() ...
		return circular_arc_bounding_box(cx, cy, rx, start_angle, sweep_angle, x2, y2)
	else
		--decompose and compute the bbox of the bboxes of the segments
		local x1, y1 = endpoints(cx, cy, rx, ry, start_angle, sweep_angle, rotation, x2, y2, mt)
		local bx, by, bw, bh = 1/0, 1/0, -1/0, -1/0
		local function write(_, x2, y2, x3, y3, x4, y4)
			bx, by, bw, bh = grow_bbox(bx, by, bw, bh, bezier3_bounding_box(x1, y1, x2, y2, x3, y3, x4, y4))
			x1, y1 = x4, y4
		end
		to_bezier3(write, cx, cy, rx, ry, start_angle, sweep_angle, rotation, x2, y2, mt, segment_max_sweep)
		return bx, by, bw, bh
	end
end

--from http://www.w3.org/TR/SVG/implnote.html#ArcConversionCenterToEndpoint
local function to_svgarc(cx, cy, rx, ry, start_angle, sweep_angle, rotation, x2, y2)
	local x1, y1, x2, y2 = endpoints(cx, cy, rx, ry, start_angle, sweep_angle, rotation, x2, y2)
	sweep_angle = observed_sweep(sweep_angle)
	local large = abs(sweep_angle) > 180 and 1 or 0
	local sweep = sweep_angle >= 0 and 1 or 0
	return x1, y1, rx, ry, rotation, large, sweep, x2, y2
end

--convert to a 3-point parametric arc (only for circular arcs).
local function to_arc_3p(cx, cy, rx, ry, start_angle, sweep_angle, rotation, x2, y2)
	if rx ~= ry then return end
	local x1, y1, x2, y2 = endpoints(cx, cy, rx, ry, start_angle, sweep_angle, rotation, x2, y2)
	local xp, yp = point_around(cx, cy, rx, start_angle + observed_sweep(sweep_angle) / 2)
	return x1, y1, xp, yp, x2, y2
end

return {
	observed_sweep = observed_sweep,
	sweep_between = sweep_between,
	sweep_time = sweep_time,
	is_sweeped = is_sweeped,
	endpoints = endpoints,
	to_svgarc = to_svgarc,
	to_arc_3p = to_arc_3p,
	best_segment_max_sweep = best_segment_max_sweep,
	point_at = point_at,
	tangent_vector = tangent_vector,
	bounding_box = bounding_box,
	length = length,
	--path API
	to_bezier3 = to_bezier3,
	point = point,
	hit = hit,
	split = split,
}

