
--tweening & timelines for animation.
--Written by Cosmin Apreutesei. Public Domain.

if not ... then require'tweening_demo' end

--prototype-based dynamic inheritance with __call constructor (from glue).
local function object(super, o)
	o = o or {}
	o.__index = super
	o.__call = super and super.__call
	return setmetatable(o, o)
end

local function clamp(x, min, max) --from glue
	return math.min(math.max(x, min), max)
end

local function lerp(d, x1, x2) --lerp from 0..1 to x1..x2
	return x1 + d * (x2 - x1)
end

--remove entries marked `false` from an array efficiently.
local function cleanup(t)
	local j
	--move marked entries to the end
	for i=1,#t do
		if not t[i] then
			j = j or i
		elseif j then
			t[j] = t[i]
			t[i] = false
			j = j + 1
		end
	end
	--remove marked entries without creating gaps
	if j then
		for i=#t,j,-1 do
			t[i] = nil
		end
		assert(#t == j-1)
	end
end

local function copy(t, dt)
	local dt = dt or {}
	if t then
		for k,v in pairs(t) do
			dt[k] = v
		end
	end
	return dt
end

--module ---------------------------------------------------------------------

local tw = object()

tw.interpolate = {}     --{type -> interpolate_function}
tw._type = {}           --{attr -> type}
tw.type = {}            --{patt|f(attr) -> type}

function tw.__call(super)
	local self = object(super)
	self._clock = false --avoid inheriting it
	self.interpolate = object(super.interpolate)
	self._type = object(super._type)
	self.type = object(super.type)
	return self
end

--relative values operations
local op = {}
op['+'] = function(b, a) return a + b end
op['-'] = function(b, a) return a - b end
op['*'] = function(b, a) return a * b end

--directional rotations
local rot = {}
function rot.cw(b, a) return a > b and b + 2 * math.pi or b end
function rot.ccw(b, a) return a < b and b - 2 * math.pi or b end
function rot.short(b, a) --shortest sweep
	local b_cw = rot.cw(b, a)
	local b_ccw = rot.ccw(b, a)
	return math.abs(a - b_cw) < math.abs(a - b_ccw) and b_cw or b_ccw
end

--unit conversions
local unit = {}
unit['%'] = function(b, a) return b / 100 * a end
unit.ms = function(b, a) return b / 1000 end
unit.deg = function(b, a) return math.rad(b) end

--note: tween is not used here but overrides could use it.
function tw:parse_value(b, a, tween)
	if b == nil then
		return a
	elseif type(b) == 'string' then
		local op_, unit_, rot_
		b = b:gsub('^([-+*])=', function(s)
			op_ = op[s]
			if op_ then return '' end
		end)
		b = b:gsub('_?([%a]+)$', function(s)
			rot_ = rot[s]
			if rot_ then return '' end
		end)
		b = b:gsub('[%a%%]+$', function(s)
			unit_ = unit[s]
			if unit_ then return '' end
		end)
		b = assert(tonumber(b), 'invalid value')
		if unit_ then b = unit_(b, a) end
		if op_ then b = op_(b, a) end
		if rot_ then b = rot_(b, a) end
	elseif type(b) == 'function' then
		b = b(a, tween)
	end
	return b
end

--note: loop_index is not used here but overrides could use it.
function tw:ease(ease, way, progress, loop_index, ...)
	local easing = require'easing'
	return easing.ease(ease, way, progress, ...)
end

--find an attribute type based on its name
function tw:_attr_type(attr_type, attr)
	local attr_type = attr_type or self._type[attr]
	if not attr_type then
		for patt, atype in pairs(self.type) do
			if (type(patt) == 'string' and attr:find(patt))
				or (type(patt) ~= 'string' and patt(attr))
			then
				attr_type = atype
				break
			end
		end
		attr_type = attr_type or 'number'
		self._type[attr] = attr_type --cache it
	end
	return attr_type
end

function tw:interpolation_function(attr_type, attr)
	local attr_type = self:_attr_type(attr_type, attr)
	local interpolate = self.interpolate[attr_type]
	return interpolate
end

function tw:current_clock()
	return require'time'.clock()
end

function tw:clock(clock)
	if clock ~= nil then --freeze the clock
		self._clock = clock
	end
	return self._clock or self:current_clock()
end

--interpolators --------------------------------------------------------------

function tw.interpolate.number(d, x1, x2)
	return lerp(d, tonumber(x1), tonumber(x2))
end

function tw.interpolate.integer(d, i1, i2)
	return math.floor(lerp(d, tonumber(i1), tonumber(i2)) + 0.5)
end

function tw.interpolate.list(d, t1, t2, t) --colors, unpacked coord lists...
	t = t or {}
	for i=1,math.min(#t1, #t2) do
		t[i] = lerp(d, t1[i], t2[i])
	end
	return t
end

--tweens ---------------------------------------------------------------------
--A tween updates a single attribute value on a single target object.

local tween = object()
tw.tween = tween

--timing model / definition
tween.start = nil        --start clock (defaults to clock at init time)
tween.timeline = nil     --if set, start is relative to timeline.start
tween.duration = 1       --loop duration; can't be negative
tween.ease = 'quad'      --function `f(t) -> d` or name from easing module
tween.way = 'in'         --easing way: 'in', 'out', 'inout', 'outin'
tween.backwards = false  --first iteration is backwards
tween.yoyo = true        --alternate between forwards/backwards on each loop
tween.loop = 1           --repeat count; 1/0 for infinite; can be fractional
tween.speed = 1          --speed factor; can be <= 0 with caveats
tween.offset = 0         --can be fractional, negative, > 1
--timing model / state
tween.running = true     --set to false to start paused
tween.clock = nil        --current clock (set on init and on update_clock())
tween.resume_clock = nil --current clock when paused (same)

--animation model / definition
tween.target = nil       --used as v = target[attr] and target[attr] = v
tween.attr = nil
tween.from = nil         --rel/abs value at progress 0
tween.to = nil           --rel/abs value at progress 1
tween.type = nil         --force attr type
tween.interpolate = nil  --custom interpolation function

--constructors

function tween:__call(tweening, o)
	local self = object(self, o)
	self.tweening = tweening
	self:reset()
	return self
end

function tween:clone()
	return self:__call(self.tweening, copy(self))
end

function tween:reset()
	self:_init_timing_model()
	self:_init_animation_model()
end

--timing model / definition

function tween:_init_timing_model()
	self.start = self.start or self.tweening:clock()
	self.clock = self.clock or self.start
	if not self.running and not self.resume_clock then
		self.resume_clock = self.clock
	end
end

function tween:total_duration()
	return math.max(self.duration * self.loop, 0) / self.speed
end

function tween:end_clock()
	return self.start + self:total_duration()
end

function tween:is_infinite()
	return math.abs(self:total_duration()) == 1/0
end

--always returns the start clock for infinite tweens.
function tween:clock_at(progress)
	return self.start + self:total_duration() * progress
end

--check if the progress on a certain iteration should increase or decrease.
function tween:is_backwards(i)
	if self.yoyo then
		return i % 2 == (self.backwards and 0 or 1)
	else
		return self.backwards
	end
end

--timing model / state-depending

--returns where the entire animation is in the 0..1 interval.
--returns 0 for infinite tweens on any clock.
function tween:_progress(clock)
	return ((clock or self.clock) - self.start) / self:total_duration()
end

function tween:progress(clock)
	return clamp(self:_progress(clock), 0, 1)
end

function tween:status(clock)
	local p = self:_progress(clock)
	return p < 0 and 'not_started' or p >= 1 and 'finished'
		or self.running and 'running' or 'paused'
end

--linear progress in 0..loop
function tween:_loop_progress(clock)
	local clock = clock or self.clock
	local time_in = clock - self.start
	return time_in / (self.duration / self.speed)
end

function tween:loop_progress(clock)
	return clamp(self:_loop_progress(clock), 0, self.loop)
end

function tween:loop_clock_at(loop_progress)
	return self.start + loop_progress * (self.duration / self.speed)
end

--non-linear (eased) progress within current iteration (can exceed 0..1).
local empty = {}
function tween:distance(clock)
	local p = self.offset + self:loop_progress(clock)
	local i = math.floor(p)
	local p = p - i
	if self:is_backwards(i) then
		p = 1 - p
	end
	return self.tweening:ease(self.ease, self.way, p, i,
		unpack(self.ease_args or empty))
end

--timing model / state-changing

function tween:update_clock(clock)
	clock = clock or self.tweening:clock()
	if self.running then
		self.clock = clock
	else
		self.resume_clock = clock
	end
end

function tween:update(clock)
	self:update_clock(clock)
	self:update_value()
end

function tween:pause()
	self.running = false
	self.resume_clock = self.clock
end

function tween:resume()
	if self.running then return end
	self.running = true
	self.start = self.start + (self.resume_clock - self.clock)
	self:update_value()
end

function tween:seek(progress)
	if self:is_infinite() then return end
	if self.running then
		self.start = self.start
			+ (self.clock - self:clock_at(progress))
	else
		self.clock = self:clock_at(progress)
	end
	self:update_value()
end

function tween:loop_seek(loop_progress)
	if self:is_infinite() then return end
	if self.running then
		self.start = self.start + (self.clock - self:loop_clock_at(loop_progress))
	else
		self.clock = self:loop_clock_at(loop_progress)
	end
	self:update_value()
end

function tween:stop()
	self:pause()
	if self.timeline then
		self.timeline:remove(self)
	end
end

function tween:restart()
	self:loop_seek(0)
end

--these weird calculations change the timing parameters such that the shape
--of distance() gets horizontally flipped around the current clock point in
--order to look like it's going back in time while actually continuing to go
--forward.
--note: this can also be done more generally (with less knowledge of the
--timing model) by just negating `speed`, but that wouldn't work with an
--infinite tween because `start` would then be a fixed point in the future.
function tween:reverse()
	local p = self:_loop_progress()
	local loop = self.loop
	if loop == 1/0 then
		loop = math.ceil(p)
	end
	self.offset = (1 - self.offset) - (loop - math.floor(loop))
	self.start = self.start - (self:loop_clock_at(loop - p) - self.clock)
	if not self.yoyo or math.floor(loop) % 2 == 0 then
		self.backwards = not self.backwards
	end
end

--turn a finite tween into a tweenable object with the attribute
--`progress` tweenable in 0..1 and `loop_progress` tweenable in `0..loop`.
function tween:totarget()
	local t = {}
	setmetatable(t, t)
	function t.__index(t, k)
		if k == 'progress' then
			return self:progress()
		elseif k == 'loop_progress' then
			return self:loop_progress()
		else
			return rawget(self, k)
		end
	end
	function t.__newindex(t, k, v)
		if k == 'progress' then
			self:update(self:clock_at(v))
		elseif k == 'loop_progress' then
			self:update(self:loop_clock_at(v))
		else
			rawset(self, k, v)
		end
	end
	return t
end

--animation model

function tween:parse_value(v, relative_to)
	return self.tweening:parse_value(v, relative_to, self)
end

function tween:_init_animation_model()
	if not self.interpolate or self._auto_interpolate then
		self.interpolate =
			self.tweening:interpolation_function(self.type, self.attr)
		self._auto_interpolate = true
	end
	local v = self:get_value()
	local v = self.interpolate(1, v, v)
	self._v0 = self:parse_value(self.from, v)
	self._v1 = self:parse_value(self.to, v)
end

function tween:get_value()
	return self.target[self.attr]
end

function tween:set_value(v)
	self.target[self.attr] = v
end

function tween:update_value()
	local d = self:distance()
	local v = self.interpolate(d, self._v0, self._v1, self:get_value())
	self:set_value(v)
end

--timeline -------------------------------------------------------------------
--A timeline is a tween which plays a list of tweens.

local timeline = object()
tw.timeline = timeline
copy(tween, timeline) --statically inherit tween's fields

--timing model
timeline.duration = 0 --auto-increased when adding tweens
timeline.ease = 'linear'
timeline.auto_duration = true --auto-increase duration to include all tweens
timeline.auto_remove = true --remove tweens automatically when finished
timeline.tween_progress = false --interpolate the child tweens' progress

--constructors

function timeline:clone()
	local t = timeline(copy(self))
	t.tweens = copy(self.tweens)
	return t
end

function timeline:_init_animation_model() end

function timeline:reset()
	self:_init_timing_model()
	self.tweens = self.tweens or {}
	if self.auto_duration then
		self.duration = 0
	end
	for i,tween in ipairs(self.tweens) do
		tween:reset()
		self:_adjust(tween)
	end
end

function timeline:_adjust(new_tween)
	if not self.auto_duration then return end
	self.duration = math.max(self.duration, new_tween:end_clock())
end

function timeline:_add_tween(tween, start)
	table.insert(self.tweens, tween)
	if start then
		tween.start = self:parse_value(start, self:total_duration())
	elseif self:is_infinite() then
		tween.start = 0
	else
		tween.start = self:total_duration()
	end
	tween.timeline = self
	self:_adjust(tween)
	return self
end

function timeline:_add_tweens(t, start)
	local attrs = {}
	copy(t.from, attrs)
	copy(t.to, attrs)
	copy(t.cycle_from, attrs)
	copy(t.cycle_to, attrs)
	copy(t.cycle, attrs)
	start = start or t.start
	for attr in pairs(attrs) do
		local targets = t.targets or {t.target}
		local from = t.from and t.from[attr]
		local to = t.to and t.to[attr]
		local c_from = t.cycle_from or (t.from and t.cycle)
		local c_to   = t.cycle_to   or (t.to   and t.cycle)
		c_from = c_from and c_from[attr]
		c_to   = c_to   and c_to  [attr]
		for i,target in ipairs(targets) do
			local tt = copy(t)
			tt.attr = attr
			tt.from = c_from and c_from[(i - 1) % #c_from + 1] or from
			tt.to = c_to and c_to[(i - 1) % #c_to + 1] or to
			tt.target = target
			tt.start = 0
			tt.cycle_from = nil
			tt.cycle_to = nil
			tt.cycle = nil
			local tween = self.tweening:tween(tt)
			self:add(tween, start)
			start = tween.start --parsed start, same for all tweens
		end
	end
	return self
end

function timeline:add(t, start)
	if t.__index then
		return self:_add_tween(t, start)
	elseif t.from or t.to or t.cycle_from or t.cycle_to or t.cycle then
		return self:_add_tweens(t, start)
	else
		error'invalid arguments'
	end
end

function timeline:each(func, ...)
	local removed
	for i,tween in ipairs(self.tweens) do
		local ret, brk = func(tween, ...)
		if ret == false then break end
		if ret == 'remove' then --remove tween
			self.tweens[i] = false
			tween.timeline = false
			removed = true
		elseif ret then --replace tween
			self.tweens[i] = ret
			ret.timeline = self
		end
		if brk then break end
		if tween.tweens then
			tween:each(func, ...)
		end
	end
	if removed then
		cleanup(self.tweens)
	end
end

function timeline:remove(what)
	self:each(function(tween)
		if (type(what) == 'function' and what(tween))
			or tween == what
			or tween.id == what
			or tween.attr == what
			or tween.target == what
		then
			return 'remove'
		end
	end)
end

function timeline:clear()
	self:each(function(tween, timeline)
		if tween.timeline == timeline then
			return 'remove'
		else
			return false
		end
	end, self)
end

--timing model

function timeline:status(clock)
	if #self.tweens == 0 then
		return 'empty'
	end
	return tween.status(self, clock)
end

--animation model

timeline.get_value = nil --not supported
timeline.set_value = nil --not supported

--note: auto_remove not enabled in this mode.
function timeline:_interpolate_tweens()
	local d = self:distance()
	for i,tween in ipairs(self.tweens) do
		tween:update(tween:clock_at(d))
	end
end

function timeline:_update_tweens()
	local clock = self.clock
	local status = self:status()
	if status == 'finished' then
		clock = self:end_clock()
	elseif status == 'not_started' then
		clock = self.start
	end
	local clock = (clock - self.start) * self.speed
	local removed
	for i,tween in ipairs(self.tweens) do
		tween:update(clock)
		if self.auto_remove and tween:status(clock) == 'finished' then
			self.tweens[i] = false
			tween.timeline = false
			removed = true
		end
	end
	if removed then
		cleanup(self.tweens)
	end
end

function timeline:update_value()
	if #self.tweens == 0 then return end
	if self.tween_progress then
		self:_interpolate_tweens()
	else
		self:_update_tweens()
	end
end

return tw
