-------------------------------------------------------------------------------
-- Spine Runtimes Software License v2.5
--
-- Copyright (c) 2013-2016, Esoteric Software
-- All rights reserved.
--
-- You are granted a perpetual, non-exclusive, non-sublicensable, and
-- non-transferable license to use, install, execute, and perform the Spine
-- Runtimes software and derivative works solely for personal or internal
-- use. Without the written permission of Esoteric Software (see Section 2 of
-- the Spine Software License Agreement), you may not (a) modify, translate,
-- adapt, or develop new applications using the Spine Runtimes or otherwise
-- create derivative works or improvements of the Spine Runtimes or (b) remove,
-- delete, alter, or obscure any trademarks or any copyright, trademark, patent,
-- or other intellectual property or proprietary rights notices on or in the
-- Software, including any copy thereof. Redistributions in binary or source
-- form must include this license and terms.
--
-- THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
-- IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-- MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
-- EVENT SHALL ESOTERIC SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
-- PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
-- USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
-- IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-- ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-- POSSIBILITY OF SUCH DAMAGE.
-------------------------------------------------------------------------------

-- FIXME the logic in this file uses 0-based indexing. Each array
-- access adds 1 to the calculated index. We should switch the logic
-- to 1-based indexing eventually.

local setmetatable = setmetatable
local AttachmentType = require "spine-lua.attachments.AttachmentType"
local PathConstraintData = require "spine-lua.PathConstraintData"
local utils = require "spine-lua.utils"
local math_pi = math.pi
local math_pi2 = math.pi * 2
local math_atan2 = math.atan2
local math_sqrt = math.sqrt
local math_acos = math.acos
local math_sin = math.sin
local math_cos = math.cos
local table_insert = table.insert
local math_deg = math.deg
local math_rad = math.rad
local math_abs = math.abs
local math_max = math.max

local PathConstraint = {}
PathConstraint.__index = PathConstraint

PathConstraint.NONE = -1
PathConstraint.BEFORE = -2
PathConstraint.AFTER = -3
PathConstraint.epsilon = 0.00001

function PathConstraint.new (data, skeleton)
	if not data then error("data cannot be nil", 2) end
	if not skeleton then error("skeleton cannot be nil", 2) end

	local self = {
		data = data,
		bones = {},
		target = skeleton:findSlot(data.target.name),
		position = data.position,
		spacing = data.spacing,
		rotateMix = data.rotateMix,
		translateMix = data.translateMix,
		spaces = {},
		positions = {},
		world = {},
		curves = {},
		lengths = {},
		segments = {}
	}
	setmetatable(self, PathConstraint)

	for i,boneData in ipairs(data.bones) do
		table_insert(self.bones, skeleton:findBone(boneData.name))
	end

	return self
end

function PathConstraint:apply ()
	self:update()
end

function PathConstraint:update ()
	local attachment = self.target.attachment
	if not attachment or not (attachment.type == AttachmentType.path) then return end

	local rotateMix = self.rotateMix
	local translateMix = self.translateMix
	local translate = translateMix > 0
	local rotate = rotateMix > 0
	if not translate and not rotate then return end

	local data = self.data;
	local spacingMode = data.spacingMode
	local lengthSpacing = spacingMode == PathConstraintData.SpacingMode.length
	local rotateMode = data.rotateMode
	local tangents = rotateMode == PathConstraintData.RotateMode.tangent
	local scale = rotateMode == PathConstraintData.RotateMode.chainscale
	local bones = self.bones
	local boneCount = #bones
	local spacesCount = boneCount + 1
	if tangents then spacesCount = boneCount end
	local spaces = utils.setArraySize(self.spaces, spacesCount)
	local lengths = nil
	local spacing = self.spacing
	if scale or lengthSpacing then
		if scale then lengths = utils.setArraySize(self.lengths, boneCount) end
		local i = 0
		local n = spacesCount - 1
		while i < n do
			local bone = bones[i + 1];
			local setupLength = bone.data.length
      if setupLength < PathConstraint.epsilon then
        if scale then lengths[i + 1] = 0 end
        i = i + 1
        spaces[i + 1] = 0
      else
   			local x = setupLength * bone.a
        local y = setupLength * bone.c
        local length = math_sqrt(x * x + y * y)
        if scale then lengths[i + 1] = length end
        i = i + 1
        if lengthSpacing then 
          spaces[i + 1] = (setupLength + spacing) * length / setupLength
        else
          spaces[i + 1] = spacing * length / setupLength
        end
      end
		end
	else
		local i = 1
		while i < spacesCount do
			spaces[i + 1] = spacing
			i = i + 1
		end
	end

	local positions = self:computeWorldPositions(attachment, spacesCount, tangents, data.positionMode == PathConstraintData.PositionMode.percent, spacingMode == PathConstraintData.SpacingMode.percent)
	local boneX = positions[1]
	local boneY = positions[2]
	local offsetRotation = data.offsetRotation
	local tip = false;
	if offsetRotation == 0 then
			tip = rotateMode == PathConstraintData.RotateMode.chain
	else
		tip = false;
		local p = self.target.bone;
		if p.a * p.d - p.b * p.c > 0 then
			offsetRotation = offsetRotation * utils.degRad
		else
			offsetRotation = offsetRotation * -utils.degRad
		end
	end

	local i = 0
	local p = 3
	while i < boneCount do
		local bone = bones[i + 1]
		bone.worldX = bone.worldX + (boneX - bone.worldX) * translateMix
		bone.worldY = bone.worldY + (boneY - bone.worldY) * translateMix
		local x = positions[p + 1]
		local y = positions[p + 2]
		local dx = x - boneX
		local dy = y - boneY
		if scale then
			local length = lengths[i + 1]
			if length ~= 0 then
				local s = (math_sqrt(dx * dx + dy * dy) / length - 1) * rotateMix + 1
				bone.a = bone.a * s
				bone.c = bone.c * s
			end
		end
		boneX = x
		boneY = y
		if rotate then
			local a = bone.a
			local b = bone.b
			local c = bone.c
			local d = bone.d
			local r = 0
			local cos = 0
			local sin = 0
			if tangents then
				r = positions[p - 1 + 1]
			elseif spaces[i + 1 + 1] == 0 then
				r = positions[p + 2 + 1]
			else
				r = math_atan2(dy, dx)
			end
			r = r - math_atan2(c, a)			
			if tip then
				cos = math_cos(r)
				sin = math_sin(r)
				local length = bone.data.length
				boneX = boneX + (length * (cos * a - sin * c) - dx) * rotateMix;
				boneY = boneY + (length * (sin * a + cos * c) - dy) * rotateMix;
			else
				r = r + offsetRotation
			end
			if r > math_pi then
				r = r - math_pi2
			elseif r < -math_pi then
				r = r + math_pi2
			end
			r = r * rotateMix
			cos = math_cos(r)
			sin = math.sin(r)
			bone.a = cos * a - sin * c
			bone.b = cos * b - sin * d
			bone.c = sin * a + cos * c
			bone.d = sin * b + cos * d
		end
		bone.appliedValid = false
		i = i + 1
		p = p + 3
	end
end

function PathConstraint:computeWorldPositions (path, spacesCount, tangents, percentPosition, percentSpacing)
	local target = self.target
	local position = self.position
	local spaces = self.spaces
	local out = utils.setArraySize(self.positions, spacesCount * 3 + 2)
	local world = nil
	local closed = path.closed
	local verticesLength = path.worldVerticesLength
	local curveCount = verticesLength / 6
	local prevCurve = PathConstraint.NONE

	if not path.constantSpeed then
		local lengths = path.lengths
		if closed then curveCount = curveCount - 1 else curveCount = curveCount - 2 end
		local pathLength = lengths[curveCount + 1];
		if percentPosition then position = position * pathLength end
		if percentSpacing then
			local i = 0
			while i < spacesCount do
				spaces[i + 1] = spaces[i + 1] * pathLength
				i = i + 1
			end
		end
		world = utils.setArraySize(self.world, 8);
		local i = 0
		local o = 0
		local curve = 0
		while i < spacesCount do
			local space = spaces[i + 1];
			position = position + space
			local p = position

			local skip = false
			if closed then
				p = p % pathLength
				if p < 0 then p = p + pathLength end
				curve = 0
			elseif p < 0 then
				if prevCurve ~= PathConstraint.BEFORE then
					prevCurve = PathConstraint.BEFORE
					path:computeWorldVertices(target, 2, 4, world, 0, 2)
				end
				self:addBeforePosition(p, world, 0, out, o)
				skip = true
			elseif p > pathLength then
				if prevCurve ~= PathConstraint.AFTER then
					prevCurve = PathConstraint.AFTER
					path:computeWorldVertices(target, verticesLength - 6, 4, world, 0, 2)
				end
				self:addAfterPosition(p - pathLength, world, 0, out, o)
				skip = true
			end

			if not skip then
				-- Determine curve containing position.
				while true do
					local length = lengths[curve + 1]
					if p <= length then
						if curve == 0 then
							p = p / length
						else
							local prev = lengths[curve - 1 + 1]
							p = (p - prev) / (length - prev)
						end
						break
					end
					curve = curve + 1
				end
				if curve ~= prevCurve then
					prevCurve = curve
					if closed and curve == curveCount then
						path:computeWorldVertices(target, verticesLength - 4, 4, world, 0, 2)
						path:computeWorldVertices(target, 0, 4, world, 4, 2)
					else
						path:computeWorldVertices(target, curve * 6 + 2, 8, world, 0, 2)
					end
				end
				self:addCurvePosition(p, world[1], world[2], world[3], world[4], world[5], world[6], world[7], world[8], out, o, tangents or (i > 0 and space == 0))
			end

			i = i + 1
			o = o + 3
		end
		return out
	end

	-- World vertices.
	if closed then
		verticesLength = verticesLength + 2
		world = utils.setArraySize(self.world, verticesLength)
		path:computeWorldVertices(target, 2, verticesLength - 4, world, 0, 2)
		path:computeWorldVertices(target, 0, 2, world, verticesLength - 4, 2)
		world[verticesLength - 2 + 1] = world[0 + 1]
		world[verticesLength - 1 + 1] = world[1 + 1]
	else
		curveCount = curveCount - 1
		verticesLength = verticesLength - 4;
		world = utils.setArraySize(self.world, verticesLength)
		path:computeWorldVertices(target, 2, verticesLength, world, 0, 2)
	end

	-- Curve lengths.
	local curves = utils.setArraySize(self.curves, curveCount)
	local pathLength = 0;
	local x1 = world[0 + 1]
	local y1 = world[1 + 1]
	local cx1 = 0
	local cy1 = 0
	local cx2 = 0
	local cy2 = 0
	local x2 = 0
	local y2 = 0
	local tmpx = 0
	local tmpy = 0
	local dddfx = 0
	local dddfy = 0
	local ddfx = 0
	local ddfy = 0
	local dfx = 0
	local dfy = 0
	i = 0
	local w = 2
	while i < curveCount do
		cx1 = world[w + 1]
		cy1 = world[w + 2]
		cx2 = world[w + 3]
		cy2 = world[w + 4]
		x2 = world[w + 5]
		y2 = world[w + 6]
		tmpx = (x1 - cx1 * 2 + cx2) * 0.1875
		tmpy = (y1 - cy1 * 2 + cy2) * 0.1875
		dddfx = ((cx1 - cx2) * 3 - x1 + x2) * 0.09375
		dddfy = ((cy1 - cy2) * 3 - y1 + y2) * 0.09375
		ddfx = tmpx * 2 + dddfx
		ddfy = tmpy * 2 + dddfy
		dfx = (cx1 - x1) * 0.75 + tmpx + dddfx * 0.16666667
		dfy = (cy1 - y1) * 0.75 + tmpy + dddfy * 0.16666667
		pathLength = pathLength + math_sqrt(dfx * dfx + dfy * dfy)
		dfx = dfx + ddfx
		dfy = dfy + ddfy
		ddfx = ddfx + dddfx
		ddfy = ddfy + dddfy
		pathLength = pathLength + math_sqrt(dfx * dfx + dfy * dfy)
		dfx = dfx + ddfx
		dfy = dfy + ddfy
		pathLength = pathLength + math_sqrt(dfx * dfx + dfy * dfy)
		dfx = dfx + ddfx + dddfx
		dfy = dfy + ddfy + dddfy
		pathLength = pathLength + math_sqrt(dfx * dfx + dfy * dfy)
		curves[i + 1] = pathLength
		x1 = x2
		y1 = y2
		i = i + 1
		w = w + 6
	end
	if percentPosition then position = position * pathLength end
	if percentSpacing then
		local i = 0
		while i < spacesCount do
			spaces[i + 1] = spaces[i + 1] * pathLength
			i = i + 1
		end
	end

	local segments = self.segments
	local curveLength = 0
	local i = 0
	local o = 0
	local curve = 0
	local segment = 0
	while i < spacesCount do
		local space = spaces[i + 1]
		position = position + space
		local p = position

		local skip = false
		if closed then
			p = p % pathLength
			if p < 0 then p = p + pathLength end
			curve = 0
		elseif p < 0 then
			self:addBeforePosition(p, world, 0, out, o)
			skip = true
		elseif p > pathLength then
			self:addAfterPosition(p - pathLength, world, verticesLength - 4, out, o)
			skip = true
		end

		if not skip then
			-- Determine curve containing position.
			while true do
				local length = curves[curve + 1]
				if p <= length then
					if curve == 0 then
						p = p / length
					else
						local prev = curves[curve - 1 + 1]
						p = (p - prev) / (length - prev)
					end
					break
				end
				curve = curve + 1
			end

			-- Curve segment lengths.
			if curve ~= prevCurve then
				prevCurve = curve
				local ii = curve * 6
				x1 = world[ii + 1]
				y1 = world[ii + 2]
				cx1 = world[ii + 3]
				cy1 = world[ii + 4]
				cx2 = world[ii + 5]
				cy2 = world[ii + 6]
				x2 = world[ii + 7]
				y2 = world[ii + 8]
				tmpx = (x1 - cx1 * 2 + cx2) * 0.03
				tmpy = (y1 - cy1 * 2 + cy2) * 0.03
				dddfx = ((cx1 - cx2) * 3 - x1 + x2) * 0.006
				dddfy = ((cy1 - cy2) * 3 - y1 + y2) * 0.006
				ddfx = tmpx * 2 + dddfx
				ddfy = tmpy * 2 + dddfy
				dfx = (cx1 - x1) * 0.3 + tmpx + dddfx * 0.16666667
				dfy = (cy1 - y1) * 0.3 + tmpy + dddfy * 0.16666667
				curveLength = math_sqrt(dfx * dfx + dfy * dfy)
				segments[1] = curveLength
				ii = 1
				while ii < 8 do
					dfx = dfx + ddfx
					dfy = dfy + ddfy
					ddfx = ddfx + dddfx
					ddfy = ddfy + dddfy
					curveLength = curveLength + math_sqrt(dfx * dfx + dfy * dfy)
					segments[ii + 1] = curveLength
					ii = ii + 1
				end
				dfx = dfx + ddfx
				dfy = dfy + ddfy
				curveLength = curveLength + math_sqrt(dfx * dfx + dfy * dfy)
				segments[9] = curveLength
				dfx = dfx + ddfx + dddfx
				dfy = dfy + ddfy + dddfy
				curveLength = curveLength + math_sqrt(dfx * dfx + dfy * dfy)
				segments[10] = curveLength
				segment = 0
			end

			-- Weight by segment length.
			p = p * curveLength
			while true do
				local length = segments[segment + 1]
				if p <= length then
					if segment == 0 then
						p = p / length
					else
						local prev = segments[segment - 1 + 1]
						p = segment + (p - prev) / (length - prev)
					end
					break;
				end
				segment = segment + 1
			end
			self:addCurvePosition(p * 0.1, x1, y1, cx1, cy1, cx2, cy2, x2, y2, out, o, tangents or (i > 0 and space == 0))
		end

		i = i + 1
		o = o + 3
	end
	return out
end

function PathConstraint:addBeforePosition (p, temp, i, out, o)
	local x1 = temp[i + 1]
	local y1 = temp[i + 2]
	local dx = temp[i + 3] - x1
	local dy = temp[i + 4] - y1
	local r = math_atan2(dy, dx)
	out[o + 1] = x1 + p * math_cos(r)
	out[o + 2] = y1 + p * math_sin(r)
	out[o + 3] = r
end

function PathConstraint:addAfterPosition(p, temp, i, out, o)
	local x1 = temp[i + 3]
	local y1 = temp[i + 4]
	local dx = x1 - temp[i + 1]
	local dy = y1 - temp[i + 2]
	local r = math_atan2(dy, dx)
	out[o + 1] = x1 + p * math_cos(r)
	out[o + 2] = y1 + p * math_sin(r)
	out[o + 3] = r
end

function PathConstraint:addCurvePosition(p, x1, y1, cx1, cy1, cx2, cy2, x2, y2, out, o, tangents)
	if p == 0 or (p ~= p) then p = 0.0001 end
	local tt = p * p
	local ttt = tt * p
	local u = 1 - p
	local uu = u * u
	local uuu = uu * u
	local ut = u * p
	local ut3 = ut * 3
	local uut3 = u * ut3
	local utt3 = ut3 * p
	local x = x1 * uuu + cx1 * uut3 + cx2 * utt3 + x2 * ttt
	local y = y1 * uuu + cy1 * uut3 + cy2 * utt3 + y2 * ttt
	out[o + 1] = x
	out[o + 2] = y
	if tangents then out[o + 3] = math_atan2(y - (y1 * uu + cy1 * ut * 2 + cy2 * tt), x - (x1 * uu + cx1 * ut * 2 + cx2 * tt)) end
end

return PathConstraint
