
local ns = artifact

ns.hud_types = {}
ns.Element = {
    animate = function(e, target)
        if not e.targets then e.targets = {} end
        local time = minetest.get_us_time()
        for k, v in pairs(target) do
            e.targets[k] = {
                ref = {
                    time = time,
                    value = e[k]
                },
                target = {
                    time = time +(v.duration or 1) *1000000,
                    value = v.value
                },
                ease_fn = v.ease_fn
            }
        end
    end
}

local function bezier_ease(t, x1, y1, x2, y2)
    if t <= 0 then return 0 end
    if t >= 1 then return 1 end

    local low = 0
    local high = 1
    local epsilon = 1e-6
    local iterations = 0
    while (high - low > epsilon) and (iterations < 100) do
        local mid = (low + high) / 2
        local x = 3 * mid * (1 - mid) ^ 2 * x1 + 3 * mid ^ 2 * (1 - mid) * x2 + mid ^ 3
        if x < t then
            low = mid
        else
            high = mid
        end
        iterations = iterations + 1
    end
    local u = (low + high) / 2

    local y = 3 * u * (1 - u) ^ 2 * y1 + 3 * u ^ 2 * (1 - u) * y2 + u ^ 3
    return y
end

local function interpolate(ref, target, t, x1, y1, x2, y2)
    local eased_t = bezier_ease(t, x1 or 0, y1 or 0, x2 or 1, y2 or 1)
    return ref + (target - ref) * eased_t
end
artifact.interpolate = interpolate

function ns.register_hud_type(def)
    ns.hud_types[def.name] = setmetatable(def, {__index = ns.Element})
end

function ns.validate_type(elem, type)
    if not ns.hud_types[elem.type] then
        warn("Unknown HUD type `"..type.."` for element `"..elem.name.."`; ignoring.")
        return false
    end
    if ns.hud_types[type].required_fields then
        for _, field in ipairs(ns.hud_types[type].required_fields) do
            if elem[field] == nil then return false end
        end
    end
    return true
end

function ns.hud_add(m, def)
    if not ns.validate_type(def, def.type) then
        return false
    end
    local type = ns.hud_types[def.type]
    if type.defaults then
        def = extend(table.copy(type.defaults), def)
    end
    -- Create a random name if none is given, since the
    -- assumption is that the user doesn't care about the name.
    if not def.name then
        def.name = ""..math.random()
    end
    local el
    if m.hud[def.name] then
        el = m.hud[def.name]
        -- Simply write all modified fields to the existing element.
        extend(el, def)
    else
        el = setmetatable(def, {__index = type})
        m.hud[def.name] = el
        el:add(m)
    end
    return el
end

function ns.update_poi(m)
    for _, x in pairs(m.poi) do
        x:remove(m)
    end
    m.poi = {}
    for _, x in ipairs(minetest.find_nodes_in_area(m.pos:offset(-100, -100, -100), m.pos:offset(100,100,100), "group:poi")) do
        m.poi[#m.poi +1] = ns.hud_add(m, {
            name = "poi:"..x:to_string(),
            type = "poi",
            world_pos = x,
        })
    end
end

local default_ease_fn = {0,0,1,1}
minetest.register_globalstep(function(dtime)
    local time = minetest.get_us_time()
    for _, m in pairs(artifact.players) do
        for k, el in pairs(m.hud) do
            if el.remove_after then
                el.remove_after = el.remove_after -dtime
                if el.remove_after < 0 then
                    el:remove(m)
                    m.hud[k] = nil
                end
            end
            
            if el.targets and next(el.targets) then
                local changes = {}
                for key, target in pairs(el.targets) do
                    local fac = (time -target.ref.time) /(target.target.time -target.ref.time)
                    local ease_fn = target.ease_fn or default_ease_fn
                    local value
                    if el.field_types[key] == "vec2" then
                        value = {
                            x = interpolate(target.ref.value.x, target.target.value.x, fac, ease_fn[1], ease_fn[2], ease_fn[3], ease_fn[4]),
                            y = interpolate(target.ref.value.y, target.target.value.y, fac, ease_fn[1], ease_fn[2], ease_fn[3], ease_fn[4])
                        }
                        if fac >= 1 then
                            el.targets[key] = nil
                        end
                    elseif el.field_types[key] == "color" then
                        value = {
                            r = interpolate(target.ref.value.r, target.target.value.r, fac, ease_fn[1], ease_fn[2], ease_fn[3], ease_fn[4]),
                            g = interpolate(target.ref.value.g, target.target.value.g, fac, ease_fn[1], ease_fn[2], ease_fn[3], ease_fn[4]),
                            b = interpolate(target.ref.value.b, target.target.value.b, fac, ease_fn[1], ease_fn[2], ease_fn[3], ease_fn[4]),
                            a = interpolate(target.ref.value.a, target.target.value.a, fac, ease_fn[1], ease_fn[2], ease_fn[3], ease_fn[4]),
                        }
                        if value.r == target.target.value.r and value.g == target.target.value.g and value.b == target.target.value.b and value.a == target.target.value.a then
                            el.targets[key] = nil
                        end
                    else
                        value = interpolate(target.ref.value, target.target.value, fac, ease_fn[1], ease_fn[2], ease_fn[3], ease_fn[4])
                        if value == target.target.value then
                            el.targets[key] = nil
                        end
                    end
                    el[key] = value
                    -- We could just set this to true, but since we already
                    -- have a new table, we might as well use it.
                    changes[key] = value
                end
                el:update(m, changes)
            end
        end
        
        for k, el in ipairs(m.poi) do
            if m.dir:distance(m.pos:direction(el.world_pos)) < 0.05 then
                el.focused = true
                el:animate {
                    scale = {
                        value = {x=2,y=2},
                        duration = 0.2,
                    }
                }
            elseif el.focused then
                el.focused = false
                el:animate {
                    scale = {
                        value = {x=1,y=1},
                        duration = 0.2,
                    }
                }
            end
        end
    end
end)

function ns.color_to_number(color)
    return tonumber(string.format("0x%.2x%.2x%.2x", color.r, color.g, color.b))
end

ns.register_hud_type {
    name = "text",
    required_fields = {"pos", "text"},
    field_types = {
        offset = "vec2",
        pos = "vec2",
        size = "vec2",
        color = "color"
    },
    defaults = {
        dir = 0,
        align = {x=0, y=0},
        offset = {x=0, y=0},
        size = {x=1, y=1},
        color = {r = 0xff, g = 0xff, b = 0xff, a = 0xff}
    },
    add = function(e, m)
        e._id = m.object:hud_add {
            type = "text",
            position = e.pos,
            direction = e.dir,
            alignment = e.align,
            offset = e.offset,
            scale = {x=100, y=100},
            size = e.size,
            text = e.text,
            number = ns.color_to_number(e.color)
        }
    end,
    update = function(e, m, changes)
        for k, v in pairs(changes) do
            if k == "color" then
                k = "number"
                v = ns.color_to_number(v)
            elseif k == "dir" then
                k = "direction"
            elseif k == "align" then
                k = "alignment"
            end
            m.object:hud_change(e._id, k, v)
        end
    end,
    remove = function(e, m)
        m.hud[e.name] = nil
        m.object:hud_remove(e._id)
        e._id = nil
        -- Prevent ongoing animations from attempting to change a nonexistent element.
        e.update = function() end
    end
}

ns.register_hud_type {
    name = "image",
    required_fields = {"pos", "image"},
    field_types = {
        offset = "vec2",
        scale = "vec2",
        pos = "vec2"
    },
    defaults = {
        dir = 0,
        align = {x=0, y=0},
        offset = {x=0, y=0},
        scale = {x=1, y=1},
        opacity = 256
    },
    add = function(e, m)
        e._id = m.object:hud_add {
            type = "image",
            position = e.pos,
            direction = e.dir,
            alignment = e.align,
            offset = e.offset,
            scale = e.scale,
            text = e.image..string.format("^[opacity:%i", e.opacity)
        }
    end,
    update = function(e, m, changes)
        for k, v in pairs(changes) do
            if k == "align" then
                k = "alignment"
            elseif k == "pos" then
                k = "position"
            elseif k == "opacity" then
                k = "text"
                v = e.image..string.format("^[opacity:%i", e.opacity)
            end
            m.object:hud_change(e._id, k, v)
        end
    end,
    remove = function(e, m)
        m.hud[e.name] = nil
        m.object:hud_remove(e._id)
        e._id = nil
        -- Prevent ongoing animations from attempting to change a nonexistent element.
        e.update = function() end
    end
}
