--@noindex
--NoIndex: true

--
-- flux
--
-- Copyright (c) 2016 rxi
--
-- This library is free software; you can redistribute it and/or modify it
-- under the terms of the MIT license. See LICENSE for details.
--

local flux = { _version = "0.1.5" }
flux.__index = flux

flux.tweens = {}
flux.easing = { linear = function(p) return p end }

local easing = {
    quad    = "p * p",
    cubic   = "p * p * p",
    quart   = "p * p * p * p",
    quint   = "p * p * p * p * p",
    expo    = "2 ^ (10 * (p - 1))",
    sine    = "-math.cos(p * (math.pi * .5)) + 1",
    circ    = "-(math.sqrt(1 - (p * p)) - 1)",
    back    = "p * p * (2.7 * p - 1.7)",
    elastic = "-(2^(10 * (p - 1)) * math.sin((p - 1.075) * (math.pi * 2) / .3))"
}

local makefunc = function(str, expr)
    local load = loadstring or load
    return load("return function(p) " .. str:gsub("%$e", expr) .. " end")()
end

for k, v in pairs(easing) do
    flux.easing[k .. "in"] = makefunc("return $e", v)
    flux.easing[k .. "out"] = makefunc([[
    p = 1 - p
    return 1 - ($e)
  ]], v)
    flux.easing[k .. "inout"] = makefunc([[
    p = p * 2
    if p < 1 then
      return .5 * ($e)
    else
      p = 2 - p
      return .5 * (1 - ($e)) + .5
    end
  ]], v)
end

local tween = {}
tween.__index = tween

local function makefsetter(field)
    return function(self, x)
        local mt = getmetatable(x)
        if type(x) ~= "function" and not (mt and mt.__call) then
            error("expected function or callable", 2)
        end
        local old = self[field]
        self[field] = old and function()
                old()
                x()
            end or x
        return self
    end
end

local function makesetter(field, checkfn, errmsg)
    return function(self, x)
        if checkfn and not checkfn(x) then
            error(errmsg:gsub("%$x", tostring(x)), 2)
        end
        self[field] = x
        return self
    end
end

tween.ease       = makesetter("_ease",
    function(x) return flux.easing[x] end,
    "bad easing type '$x'")
tween.delay      = makesetter("_delay",
    function(x) return type(x) == "number" end,
    "bad delay time; expected number")
tween.onstart    = makefsetter("_onstart")
tween.onupdate   = makefsetter("_onupdate")
tween.oncomplete = makefsetter("_oncomplete")


function tween.new(obj, time, vars)
    local self = setmetatable({}, tween)
    self.obj = obj
    self.rate = time > 0 and 1 / time or 0
    self.progress = time > 0 and 0 or 1
    self._delay = 0
    self._ease = "quadout"
    self.vars = {}
    for k, v in pairs(vars) do
        if type(v) ~= "number" then
            error("bad value for key '" .. k .. "'; expected number")
        end
        self.vars[k] = v
    end
    return self
end

function tween:init()
    for k, v in pairs(self.vars) do
        local x = self.obj[k]
        if type(x) ~= "number" then
            error("bad value on object key '" .. k .. "'; expected number")
        end
        self.vars[k] = { start = x, diff = v - x }
    end
    self.inited = true
end

function tween:after(...)
    local t
    if select("#", ...) == 2 then
        t = tween.new(self.obj, ...)
    else
        t = tween.new(...)
    end
    t.parent = self.parent
    self:oncomplete(function() flux.add(self.parent, t) end)
    return t
end

function tween:stop()
    flux.remove(self.parent, self)
end

function flux.group()
    return setmetatable({}, flux)
end

function flux:to(obj, time, vars)
    return flux.add(self, tween.new(obj, time, vars))
end

function flux:update(deltatime)
    for i = #self, 1, -1 do
        local t = self[i]
        if t._delay > 0 then
            t._delay = t._delay - deltatime
        else
            if not t.inited then
                flux.clear(self, t.obj, t.vars)
                t:init()
            end
            if t._onstart then
                t._onstart()
                t._onstart = nil
            end
            t.progress = t.progress + t.rate * deltatime
            local p = t.progress
            local x = p >= 1 and 1 or flux.easing[t._ease](p)
            for k, v in pairs(t.vars) do
                t.obj[k] = v.start + x * v.diff
            end
            if t._onupdate then t._onupdate() end
            if p >= 1 then
                flux.remove(self, i)
                if t._oncomplete then t._oncomplete() end
            end
        end
    end
end

function flux:clear(obj, vars)
    for t in pairs(self[obj]) do
        if t.inited then
            for k in pairs(vars) do t.vars[k] = nil end
        end
    end
end

function flux:add(tween)
    -- Add to object table, create table if it does not exist
    local obj = tween.obj
    self[obj] = self[obj] or {}
    self[obj][tween] = true
    -- Add to array
    table.insert(self, tween)
    tween.parent = self
    return tween
end

function flux:remove(x)
    if type(x) == "number" then
        -- Remove from object table, destroy table if it is empty
        local obj = self[x].obj
        self[obj][self[x]] = nil
        if not next(self[obj]) then self[obj] = nil end
        -- Remove from array
        self[x] = self[#self]
        return table.remove(self)
    end
    for i, v in ipairs(self) do
        if v == x then
            return flux.remove(self, i)
        end
    end
end

local bound = {
    to     = function(...) return flux.to(flux.tweens, ...) end,
    update = function(...) return flux.update(flux.tweens, ...) end,
    remove = function(...) return flux.remove(flux.tweens, ...) end,
}
setmetatable(bound, flux)

return bound
