
player_model = {}
player_model.on_changed = {}
player_model.pl = {}
local pl = 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=451, y=470},
        loop = true,
        blend = 0.2,
    },
    sit = {
        frames = {x=475, y=484},
        loop = true,
        blend = 0.2,
    },
    sitpunch = {
        frames = {x=490, y=499},
        loop = true,
        blend = 0.2,
    },
    sitaim = {
        frames = {x=505, y=514},
        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

--[[
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.6

local sneak_offset = -(12/16)*10

local tpv_camera_offset = vector.new(13, 12 + ((player_height_mult-1)*16), -5)

local collisionbox = function()
    return {
    -0.3,  0.0, -0.3,
     0.3,  1.65*player_height_mult,  0.3}
end

local function to_standing(player)
    pl[player].was_sneaking = false
    player:set_properties({
        collisionbox = collisionbox(),
    })
    player:set_eye_offset(vector.new(0, 0 + ((player_height_mult-1)*16) - 1, 0), tpv_camera_offset)
end
local function to_sneaking(player)
    pl[player].was_sneaking = true
    local cbox = collisionbox()
    cbox[5] = cbox[5] - (12/16)
    player:set_properties({
        collisionbox = cbox,
    })
    player:set_eye_offset({x=0, y= sneak_offset + ((player_height_mult-1)*16) - 1, z=0}, vector.add(tpv_camera_offset, vector.new(0, 0, 0)))
end

minetest.register_on_joinplayer(function(player)
    player_model.pl[player] = get_player_default()
    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 = 72.0,
    })
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 ov = pl[player].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)
    for i, name in pairs(pl[player].overrides) do
        if name == animname then
            table.remove(pl[player].overrides, i)
            return
        end
    end
end

function player_model.set_anim(player, an)
    if pl[player].tags[an.tag] ~= nil then return false end
    pl[player].changed_this_step = true
    pl[player].tags[an.tag] = an
    for _, act in pairs(an.actions) do
        if pl[player].actions[act] ~= nil then
            pl[player].actions[act] = pl[player].actions[act] + 1
        end
    end
end
function player_model.unset_anim(player, tagname)
    local an = pl[player].tags[tagname]
    if an == nil then return false end
    pl[player].changed_this_step = true
    for _, act in pairs(an.actions) do
        if pl[player].actions[act] ~= nil then
            pl[player].actions[act] = pl[player].actions[act] - 1
        end
    end
    pl[player].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 + 1.6
    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 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 pl[player].was_sneaking) and (not can_stand) then
        to_sneaking(player)
        -- minetest.chat_send_all("sneaking under roof now")
    elseif pl[player].was_sneaking and can_stand and not pli.ctrl.sneak then
        to_standing(player)
        -- minetest.chat_send_all("standing up now")
    else
        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 = ct.is_sneaking or not can_stop_sneaking(player)
    -- check movement stuff for basic anims
    local attached = player:get_attach() ~= nil
    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 ct = player_info.get(player)
    if not ct then return end
    local a = pl[player].actions
    local ani = ""

    if #pl[player].overrides > 0 then
        ani = pl[player].overrides[#pl[player].overrides].override
    end

    local is_sneaking = false

    if not anim[ani] then

        -- check tag list for animation requests
        if 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 ~= pl[player].anim then
            changed_animation = true
            player_model.on_changed_animation(player, pl[player].anim, ani)
            pl[player].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 pl[player].anim == "idle" then
            changed_animation = true
            player_model.on_changed_animation(player, pl[player].anim, "idle")
            pl[player].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
        to_sneaking(player)
    end
end

function player_model.fix_bones(player)
    if pl[player].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
        player_model.do_move_checks(player, dtime)
        if pl[player].changed_this_step then
            player_model.do_animations(player, dtime)
        end

        local ct = player_info.get(player)
        if ct then
            if ct.just_pressed.aux1 then
                player:set_properties({
                    stepheight = 1.2,
                })
            elseif ct.just_released.aux1 then
                player:set_properties({
                    stepheight = 0.7,
                })
            end
        end

        pl[player].changed_this_step = do_stand_sneak_check(player)
    end
end

minetest.register_globalstep(player_model.on_step)
