
player_model = {}
player_model.on_changed = {}
player_model.pl = {}

local anim = {
    idle = {
        frames = {x=1, y=60},
        loop = true,
        blend = 0.2,
    },
    walk = {
        frames = {x=71, y=90},
        loop = true,
        blend = 0.1,
    },
    punch = {
        frames = {x=101, y=110},
        loop = true,
        blend = 0.1,
    },
    walkpunch = {
        frames = {x=121, y=140},
        loop = true,
        blend = 0.1,
    },
    aim = {
        frames = {x=151, y=160},
        loop = true,
        blend = 0.2,
    },
    walkaim = {
        frames = {x=161, y=180},
        loop = true,
        blend = 0.1,
    },
    sneak = {
        frames = {x=191, y=250},
        loop = true,
        blend = 0.2,
    },
    sneakwalk = {
        frames = {x=261, y=300},
        loop = true,
        blend = 0.1,
    },
    sneakwalkpunch = {
        frames = {x=311, y=350},
        loop = true,
        blend = 0.1,
    },
    sneakpunch = {
        frames = {x=361, y=400},
        loop = true,
        blend = 0.1,
    },
    sneakwalkaim = {
        frames = {x=411, y=450},
        loop = true,
        blend = 0.1,
    },
    sneakaim = {
        frames = {x=411, y=411},
        loop = true,
        blend = 0.2,
    },
    fly = {
        frames = {x=455, y=475},
        loop = true,
        blend = 0.2,
    },
    flypunch = { -- not implemented
        frames = {x=455, y=475},
        loop = true,
        blend = 0.2,
    },
    flyaim = { -- not implemented
        frames = {x=455, y=475},
        loop = true,
        blend = 0.2,
    },
    flywalk = { -- not implemented
        frames = {x=455, y=475},
        loop = true,
        blend = 0.2,
    },
    flywalkpunch = { -- not implemented
        frames = {x=455, y=475},
        loop = true,
        blend = 0.2,
    },
}

local function get_player_default()
    return {
        anim = "idle",
        tags = {},
        actions = {
            sneak = 0,
            sit   = 0,
            walk  = 0,
            punch = 0,
            aim   = 0,
        },
        overrides = {},
        changed_this_step = true,
    }
end

local pl = {}
local function check_player(player)
    local pi = pl[player]
    if not pi then pi = get_player_default(); pl[player] = pi end
    return pi
end

--[[
tag = {
    tag = "pmb_mod:item",
    actions = {"aim", "walk"},
}
]]--
local bone_defaults = {
    head2 = {
        pos = vector.new(0, 9, 0),
        rot = vector.new(-90, 0, 180) },
    armR = {
        pos = vector.new(-3, 7, 0),
        rot = vector.new(0, 0, 180) },
    armL = {},
    legR = {},
    legL = {},
    torso = {},
}

-- note: allow saving this in future
local texture_colors = {
    "#81808f", "#4c4a5e", "#4d4878", "#8f8480", "#66504d",
    "#764b38", "#ab453f", "#a76763", "#c4bbbb", "#756052",
    "#999191", "#607e57", "#326643", "#667855", "#40883f"
}
local tex_offset = math.random(#texture_colors)
local function get_texture()
    tex_offset = (tex_offset) % #texture_colors + 1
    return texture_colors[tex_offset]
end

local player_height_mult = 1.5
local default_height = 1.625

local sneak_offset = -(12/16)*10

local tpv_camera_offset = vector.new(6, -1 + ((default_height * player_height_mult)), -10)

local collisionbox = function()
    return {
    -0.3,  0.0, -0.3,
     0.3,  default_height * player_height_mult + (6/16),  0.3}
end
local selectionbox = function()
    return {
    -0.5,  0.0, -0.5,
     0.5,  default_height * player_height_mult + (6/16),  0.5}
end

function player_model.to_standing(player)
    local pi = check_player(player)
    pi.was_sneaking = false
    player:set_properties({
        collisionbox = collisionbox(),
        selectionbox = selectionbox(),
        eye_height = default_height*player_height_mult + (2/16),
    })
    player:set_eye_offset(vector.new(0, 0, 0), tpv_camera_offset)
end
function player_model.to_sneaking(player)
    local pi = check_player(player)
    pi.was_sneaking = true
    local cbox = collisionbox()
    cbox[5] = cbox[5] - (14/16)
    local sbox = selectionbox()
    sbox[5] = sbox[5] - (12/16)
    player:set_properties({
        collisionbox = cbox,
        selectionbox = sbox,
        eye_height = default_height*player_height_mult - (10/16),
    })
    player:set_eye_offset(vector.new(0, 0, 0), vector.add(tpv_camera_offset, vector.new(0, -2, 0)))
end

minetest.register_on_joinplayer(function(player)
    player_model.to_standing(player)
    -- player:set_eye_offset(vector.new(0, 0, 0), vector.new(4, 5, 0)) -- enable for slightly more usable 3rd person
    player:set_properties({
        mesh = 'humanoid.b3d',
        textures = {'clive.png^(clive_overlay.png^[multiply:'..get_texture()..")"},
        visual = "mesh",
        visual_size = {x=(player_height_mult-1)*0.5+1, y=player_height_mult},
        damage_texture_modifier = "^[colorize:red:130",
        zoom_fov = tonumber(minetest.settings:get("fov")) or 72,
    })
end)

function player_model.register_on_changed_animation(func)
    local oc = player_model.on_changed
    oc[#oc+1] = func
end
function player_model.on_changed_animation(player, fromanim, toanim)
    for i, func in pairs(player_model.on_changed) do
        func(player, fromanim, toanim)
    end
end

function player_model.set_bone_pos(player, nb)
    local def = bone_defaults[nb.bone]
    player:set_bone_position(nb.bone, vector.add(def.pos, nb.pos), vector.add(def.rot, nb.rot))
end
function player_model.reset_bone_pos(player, bone_name)
    local def = bone_defaults[bone_name]
    player:set_bone_position(bone_name, def.pos, def.rot)
end
function player_model.get_default_bone_pos(player, bone_name)
    return bone_defaults[bone_name]
end

local function table_has_value(tab, val)
    for i, v in pairs(tab) do
        if v == val then return true end
    end
    return false
end
-- DO NOT USE EXCEPT IN EXTREMELY SPECIFIC CIRCUMSTANCES AS IT CAN BREAK THE PLAYER ANIMATIONS UNTIL THEY REJOIN
function player_model.override_anim(player, animname)
    local pi = check_player(player)
    local ov = pi.overrides
    local i = table.indexof(ov, animname)
    if i ~= nil and i > 0 then
       table.remove(ov, i)
    end
    ov[#ov+1] = animname
end

function player_model.unoverride_anim(player, animname)
    local pi = check_player(player)
    for i, name in pairs(pi.overrides) do
        if name == animname then
            table.remove(pi.overrides, i)
            return
        end
    end
end

function player_model.set_anim(player, an)
    local pi = check_player(player)
    if pi.tags[an.tag] ~= nil then return false end
    pi.changed_this_step = true
    pi.tags[an.tag] = an
    for _, act in pairs(an.actions) do
        pi.actions[act] = (pi.actions[act] or 0) + 1
    end
end

function player_model.unset_anim(player, tagname)
    local pi = check_player(player)
    local an = pi.tags[tagname]
    if an == nil then return false end
    pi.changed_this_step = true
    for _, act in pairs(an.actions) do
        if pi.actions[act] ~= nil then
            pi.actions[act] = pi.actions[act] - 1
        end
    end
    pi.tags[tagname] = nil
end

local non_existent_animations = {}

local function get_eyepos(player)
    local eyepos = vector.add(player:get_pos(), vector.multiply(player:get_eye_offset(), 0.1))
    eyepos.y = eyepos.y + player:get_properties().eye_height
    return eyepos
end

local function can_stop_sneaking(player)
    local pos = player:get_pos()
    pos.y = pos.y + 1
    local eyepos_if_standing = vector.offset(pos,
        0,
        1.6 + ((player_height_mult-1)) - 1,
        0
    )
    local pointed = player_info.get_first_from_raycast(pos, eyepos_if_standing, player_info.is_solid_block)
    -- minetest.log(dump(pointed))
    return (pointed == nil)
end

local function do_stand_sneak_check(player)
    local pi = check_player(player)
    local can_stand = can_stop_sneaking(player)
    local pli = player_info.get(player)
    if not pli then return end
    pli.is_sneaking = (not can_stand) or pli.ctrl.sneak
    pli.can_sprint = can_stand

    if (not pi.was_sneaking) and (not can_stand) then
        player_model.to_sneaking(player)
        -- minetest.chat_send_all("sneaking under roof now")
    elseif pi.was_sneaking and can_stand and not pli.ctrl.sneak then
        player_model.to_standing(player)
        -- minetest.chat_send_all("standing up now")
    else
        return false
    end
    return true
end

local function sneak_allowed(player)
    local node = minetest.get_node(player:get_pos())
    local ndef = minetest.registered_nodes[node.name]
    if ndef.climbable then return false end
    return true
end

function player_model.do_move_checks(player, dtime)
    local ct = player_info.get(player)
    if not ct then return end
    ct.is_sneaking = sneak_allowed(player) and (ct.is_sneaking or not can_stop_sneaking(player))
    -- check movement stuff for basic anims
    local attached = player:get_attach()
    if attached then
        local ent = attached:get_luaentity()
        if ent and ent._no_player_animation then
            attached = nil
        end
    end
    if attached then player_model.set_anim(player, {tag="builtinsit", actions={"sneak", "sneak"}})
    else player_model.unset_anim(player, "builtinsit") end
    if (not attached) and ct.is_moving then player_model.set_anim(player, {tag="builtinwalk", actions={"walk", "walk"}})
    else player_model.unset_anim(player, "builtinwalk") end
    if ct.is_sneaking then player_model.set_anim(player, {tag="builtinsneak", actions={"sneak", "sneak"}})
    else player_model.unset_anim(player, "builtinsneak") end
    if ct.is_punching then player_model.set_anim(player, {tag="builtinpunch", actions={"punch", "punch"}})
    else player_model.unset_anim(player, "builtinpunch") end
end

function player_model.do_animations(player, dtime)
    local pi = check_player(player)
    local ct = player_info.get(player)
    if not ct then return end
    local a = pi.actions
    local ani = ""

    if #pi.overrides > 0 then
        ani = pi.overrides[#pi.overrides]
    end

    local is_sneaking = false

    if not anim[ani] then
        -- check tag list for animation requests
        if a.fly and a.fly > 0 then ani = ani .. "fly"
        elseif a.sneak > 0 and a.sneak > a.sit then ani = ani.."sneak" is_sneaking = true
        elseif a.sit > 0 then ani = ani.."sit" end
        if a.sit <= 0 and a.walk > 0 then ani = ani.."walk" end
        if a.punch > 0 and a.punch > a.aim then ani = ani.."punch"
        elseif a.aim > 0 then ani = ani.."aim" end

        if ani == "" then ani = "idle" end
    end

    -- minetest.chat_send_all(dump(is_sneaking))

    local changed_animation = false

    if anim[ani] then
        local p = anim[ani]
        if ani ~= pi.anim then
            changed_animation = true
            player_model.on_changed_animation(player, pi.anim, ani)
            pi.anim = ani
            player:set_animation(p.frames, p.fps or 24, p.blend or 0)
        end
    elseif not non_existent_animations[ani] then
        non_existent_animations[ani] = true
        minetest.log("warning", "Animation \""..ani.."\" doesn't exist, oops")
        local p = anim.idle
        if not pi.anim == "idle" then
            changed_animation = true
            player_model.on_changed_animation(player, pi.anim, "idle")
            pi.anim = "idle"
            player:set_animation(p.frames, p.fps or 24, p.blend or 0)
        end
    end
    local fpo, tpo = player:get_eye_offset()
    if changed_animation and is_sneaking then
        player_model.to_sneaking(player)
    end
end

function player_model.fix_bones(player)
    local pi = check_player(player)
    if pi.actions.aim > 0 then
        local l = player:get_look_dir()
        local bpos, brot = player:get_bone_position("head2")
        l.y = l.y * -60
        player_model.set_bone_pos(player, {
            bone = "head2",
            pos = vector.new(0, 0, 0),
            rot = vector.new(l.y, 0, 0)
        })
        player_model.set_bone_pos(player, {
            bone = "armR",
            pos = vector.new(0, 0, 0),
            rot = vector.new(l.y, 0, 0)
        })
        -- player:set_bone_position("head2", vector.new(0, 9, 0), vector.new(l.y, 0, 180))
        -- player:set_bone_position("armR", vector.new(-3, 7, 0), vector.new(l.y, 20, 180))
        -- player:set_bone_position("armL", vector.new(3, 7, 0), vector.new(l.y, -20, 180))
    else
        player_model.unset_anim(player, "armR")
        -- player_model.set_bone_pos(player, {
        --     bone = "armR",
        --     pos = vector.new(0, 0, 0),
        --     rot = vector.new(0, 0, 0)
        -- })
    end
end

function player_model.on_step(dtime)
    for _, player in pairs(minetest.get_connected_players()) do
        local pi = check_player(player)
        player_model.do_move_checks(player, dtime)
        if pi.changed_this_step then
            player_model.do_animations(player, dtime)
        end

        local ct = player_info.get(player)
        if ct then
            if (ct.ctrl.sneak or ct.ctrl.aux1) and pi.stepheight ~= 0.7 then
                pi.stepheight = 0.7
                player:set_properties({stepheight = pi.stepheight})
            elseif (not ct.ctrl.sneak) and (not ct.ctrl.aux1) and pi.stepheight ~= 1.2 then
                pi.stepheight = 1.2
                player:set_properties({stepheight = pi.stepheight})
            end
        end

        pi.changed_this_step = do_stand_sneak_check(player)
    end
end

minetest.register_globalstep(player_model.on_step)
