
local ns = artifact
ns.players = {}
local db = minetest.get_mod_storage()

include "radial_menu.lua"

Player = setmetatable({
    new = function(p)
        local m = setmetatable({
            object = p,
            pitch = 0,
            yaw = 0
        }, {__index = Player})
        
        m.name = p:get_player_name()
        m.meta = p:get_meta()
        m.character = m.meta:get("character") or "key"
        m.spawn_point = m.meta:get("spawnpoint") or artifact.origin
        m.color = m.meta:get("color")
        
        m.inv = p:get_inventory()
        m.inv:set_stack("main", 1, ItemStack("input_"..m.character))
        
        -- Generic black sky, since the whole game takes place underground.
        p:set_sky{
            type = "basic",
            base_color = "#000",
            clouds = false
        }
        p:set_sun{visible = false}
        p:set_moon{visible = false}
        p:set_stars{visible = false}
        
        p:set_properties {
            visual = "mesh",
            mesh = "artifact_character.gltf",
            shaded = false
        }
        
        p:hud_set_flags {
            healthbar = false,
            breathbar = false,
            hotbar = artifact.debug,
            minimap = false,
            basic_debug = artifact.debug,
            crosshair = false, -- It gets set to true once we can play.
            wielditem = false, -- Ditto.
            chat = false, -- We provide our own implementation of the chat HUD.
        }
        
        -- The following exists to make sure that whatever physics
        -- settings the server may have set are transparently ignored.
        local defaults = {
            speed_walk = 4,
            speed_crouch = 1.35,
            speed_fast = 20,
            speed_climb = 3,
            speed_jump = 6.5,
            gravity = 9.81,
            liquid_fluidity = 1,
            liquid_fluidity_smooth = 0.5,
            liquid_sink = 10,
            acceleration_default = 3,
            acceleration_air = 2,
            acceleration_fast = 10,
        }

        local override = {
            speed = 1,
            sneak = true,
            sneak_glitch = false,
            new_move = true,
        }

        for key, def_value in pairs(defaults) do
            local setting_name = "movement_"..key
            local current = tonumber(minetest.settings:get(setting_name)) or def_value
            override[key] = def_value /current
        end

        p:set_physics_override(override)
        
        -- No unreasonable FOV settings here.
        p:set_fov(72)
        p:set_fov(0, true, 0)
        
        -- Tell raycast collisions that we're a player.
        p:set_armor_groups(extend(p:get_armor_groups(), {player = 1}))
        
        m.hud = {}
        m.poi = {}
        m.chat = {}
        
        if not artifact.debug then
            p:set_inventory_formspec ""
        end

        if m.character == "vix" then
            artifact.apply_vix(m)
        else
            artifact.apply_key(m)
        end
        
        -- Let us build in debug mode, but ensure we always wield the hand item otherwise.
        m:set_hotbar_size(artifact.debug and 8 or 1)
        
        m.ctl = p:get_player_control()
        
        return m
    end,
    tick = function(m)
        local time = minetest.get_us_time()
        local p = m.object
        local pos = p:get_pos()
        local yaw = p:get_look_horizontal()
        local pitch = p:get_look_vertical()
        local dir = p:get_look_dir()
        local vel = p:get_velocity()
        local speed = vel:length()
        m.pos = pos
        m.pos.y = m.pos.y +m.eye_height
        
        local state = artifact.story.get_state()

        -- Sleep if we are not yet ready for the player to do things.
        if not artifact.debug and state <= artifact.story.states.init then return end
        
        -- MARK: Pointing callbacks
        
        local pointed_found = nil
        local pointed_node_found = nil
        for x in minetest.raycast(m.pos, m.pos +(dir *5)) do
            -- We should ignore all objects when placing a grabbed node.
            if x and x.type == "object" and not m._grabbed_item then
                local e = x.ref:get_luaentity()
                -- Ignore players.
                if e then
                    local names_match = m.pointed_obj and (m.pointed_obj._name or m.pointed_obj.name) == (e._name or e.name)
                    if m.pointed_obj and not names_match then
                        if m.pointed_obj.on_unhover then
                            m.pointed_obj:on_unhover(m)
                        end
                        if m.pointed_obj.on_interact and m.interaction_marker then
                            m.object:hud_remove(m.interaction_marker)
                            m.interaction_marker = nil
                            m.interaction_start = nil
                        end
                    end
                    if e.on_interact and not e._no_interact and (not names_match or names_match and not m.interaction_marker) and (not e._can_interact or e:_can_interact(m)) then
                        if m.interaction_marker then m.object:hud_remove(m.interaction_marker) end
                        local dst = e.object:get_pos()
                        if e._interact_marker_offset then dst = dst +e:_interact_marker_offset() end
                        m.interaction_marker = m.object:hud_add {
                            type = "image_waypoint",
                            world_pos = dst,
                            scale = {x=3, y=3},
                            text = "artifact_rmb.png"
                        }
                    end
                    if (m.pointed_obj and not names_match and e.on_hover) or not m.pointed_obj then
                        if e.on_hover then
                            e:on_hover(m)
                        end
                        pointed_found = true
                        m.pointed_obj = e
                        break
                    elseif m.pointed_obj and names_match then
                        pointed_found = true
                        break
                    end
                end
            elseif x and x.type == "node" then
                pointed_node_found = true
                x.node_under = minetest.get_node(x.under)
                
                local was_whackable = m.pointed_node and minetest.registered_nodes[m.pointed_node.node_under.name].groups.whackable
                local whackable = m.character == "key" and minetest.registered_nodes[x.node_under.name].groups.whackable
                if whackable and not was_whackable then
                    m.whack_hud = m.object:hud_add {
                        type = "image_waypoint",
                        world_pos = x.under,
                        scale = {x=3,y=3},
                        text = "artifact_icon_whack.png"
                    }
                elseif whackable and x.under ~= (m.pointed_node and m.pointed_node.under) then
                    m.object:hud_change(m.whack_hud, "world_pos", x.under)
                elseif not whackable and m.whack_hud then
                    m.object:hud_remove(m.whack_hud)
                    m.whack_hud = nil
                end
                m.pointed_node = x
                if m.pointed_obj then
                    if m.pointed_obj.on_unhover then
                        m.pointed_obj:on_unhover(m)
                    end
                    if m.pointed_obj.on_interact and m.interaction_marker then
                        m.object:hud_remove(m.interaction_marker)
                        m.interaction_marker = nil
                        m.interaction_start = nil
                    end
                    m.pointed_obj = nil
                end
                break
            end
        end
        if not pointed_found and m.pointed_obj then
            if m.pointed_obj.on_unhover then
                m.pointed_obj:on_unhover(m)
            end
            if m.pointed_obj.on_interact and m.interaction_marker then
                m.object:hud_remove(m.interaction_marker)
                m.interaction_marker = nil
                m.interaction_start = nil
            end
            m.pointed_obj = nil
        end
        if not pointed_node_found then
            m.pointed_node = nil
            if m.whack_hud then
                m.object:hud_remove(m.whack_hud)
                m.whack_hud = nil
            end
        end
        
        local ctl = m.object:get_player_control()
        
        -- MARK: Animations
        
        local moving = (ctl.up or ctl.down or ctl.left or ctl.right) and speed > 0.1
        if moving then
            m.moving = true
            if ctl.aux1 and ctl.up then
                if p:get_animation().y ~= 2 then p:set_animation({x=1, y=2}, 1.5, 0.2, true) end
                p:set_physics_override{
                    speed = 1.5
                }
                p:set_fov(1.1, true, 0.2)
            else
                if p:get_animation().y ~= 1 then p:set_animation({x=0, y=1}, 1.5, 0.2, true) end
                p:set_physics_override{
                    speed = 1
                }
                p:set_fov(0, true, 0.2)
            end
        else
            -- We can't call this unconditionally because during the first couple
            -- globalsteps, doing so will heavily distort the player's FOV for
            -- some reason. Since `m.moving` is never set to true until the player
            -- starts sprinting, we can sidestep this issue fairly trivially.
            if m.moving then
                p:set_physics_override{
                    speed = 1
                }
                p:set_fov(0, true, 0.2)
            end
            m.moving = false
            if p:get_animation().y ~= 0 then p:set_animation({x=0, y=0}) end
        end
        
        if not m.rot then m.rot = 0 end
        if moving then
            local fac = 0
            if ctl.left then fac = 30 elseif ctl.right then fac = -30 end
            m.rot = yaw +math.rad(fac)
        elseif math.abs(yaw -m.rot) > math.rad(40) then
            m.rot = m.rot +(yaw -(m.yaw or 0))
        end
        m.rot = m.rot %(math.pi *2)
        
        p:set_bone_override("Head", {
            rotation = {vec = vector.new(math.min(math.max(pitch, math.rad(-60)), math.rad(60)),-(yaw -m.rot),0), interpolation = 0.1, absolute = true}
        })
        
        p:set_bone_override("root", {
            rotation = {vec = vector.new(0,yaw -m.rot,0), interpolation = 0.1, absolute = true}
        })
        
        -- Handle grabbed devices. This trumps other input handling like the radial menu and on_interact.
        if m._grabbed_item then
            m._grabbed_item:move_to(m.pos +(dir *2))
            
            if ctl.place and m.pointed_node then
                m._grabbed_item:move_to(m.pointed_node.above)
                m._grabbed_item = nil
                -- This should be set dynamically by whatever function put us into the grabbing
                -- state, and accordingly should only be valid for the duration of that state.
                if m._on_ungrab then
                    m._on_ungrab()
                    m._on_ungrab = nil
                end
            end
            
            -- This code is duplicated from the bottom... but since the
            -- only cleaner alternative is goto, I decided to support PUC Lua.
            if m.next_regen and time -m.next_regen >= 0 then
                m.object:set_hp(m.object:get_hp() +1)
            end
            
            m.ctl = ctl
            m.yaw = yaw
            m.pitch = pitch
            m.dir = dir
            return
        end
        
        -- MARK: Progressive interaction
        
        if ctl.place and m.ctl.place and m.pointed_obj and m.pointed_obj.on_interact and not m.pointed_obj._no_interact and (not m.pointed_obj._can_interact or m.pointed_obj:_can_interact(m)) then
            if not m.interaction_start then
                m.interaction_start = time
            else
                local duration = (m.pointed_obj._interact_time or 1) *1000000
                local progress = (time -m.interaction_start) /duration
                if progress > 1.1 then
                    m.pointed_obj:on_interact(m)
                    m.interaction_start = nil
                    m.object:hud_remove(m.interaction_marker)
                    m.interaction_marker = nil
                elseif progress > 1 then
                    m.object:hud_change(m.interaction_marker, "text", "artifact_rmb_100.png")
                elseif progress > 0.75 then
                    m.object:hud_change(m.interaction_marker, "text", "artifact_rmb_75.png")
                elseif progress > 0.5 then
                    m.object:hud_change(m.interaction_marker, "text", "artifact_rmb_50.png")
                elseif progress > 0.25 then
                    m.object:hud_change(m.interaction_marker, "text", "artifact_rmb_25.png")
                end
            end
        elseif not ctl.place and m.interaction_start and (not m.pointed_obj or not m.pointed_obj._no_interact or m.pointed_obj._can_interact and m.pointed_obj:_can_interact(m)) then
            m.interaction_start = nil
            if m.interaction_marker then
                m.object:hud_change(m.interaction_marker, "text", "artifact_rmb.png")
            end
        end

        local wi = p:get_wielded_item()

        m.wielded_item = wi
        
        -- MARK: Radial menu handling
        
        --[[ Disabled for the present due to a dearth of usecases...
        
        -- This should only work once we have Vix, since we can't use it without her.
        if state >= artifact.story.states.main and ctl.place and not m.ctl.place and wi:get_name():find "artifact:input" and (not m.pointed_obj or not m.pointed_obj.on_interact or m.pointed_obj._no_interact) then
            artifact.show_radial_menu(m, {
                name = "construct",
                "test",
                "test2",
                "test3",
                "test4",
                "test5"
            })
        elseif m._menu and not (ctl.place and wi:get_name():find "artifact:input") or (m._menu and m.pointed_obj and m.pointed_obj.on_interact and not m.pointed_obj._no_interact) then
            local sel = m._menu[m._menu.selected]
            if sel then
                local choice = sel.item
                if choice == "test" then
                    artifact.summon_device(m, "block")
                end
            end
            artifact.dismiss_radial_menu(m, "construct")
        elseif m._menu then
            local dx = m.yaw -yaw
            local dy = m.pitch -pitch
            if dx ~= 0 and dy ~= 0 then
                m._menu.pos.x = m._menu.pos.x +dx *200
                m._menu.pos.y = m._menu.pos.y -dy *200
                local r = m._menu.pos:distance(vector.zero())
                if r > 50 then
                    r = 50
                    m._menu.pos = m._menu.pos:normalize() *50
                end
                p:hud_change(m._menu.cursor._id, "offset", m._menu.pos)
                if r > 20 then
                    local angle = minetest.dir_to_yaw(vector.new(m._menu.pos.x, 0, m._menu.pos.y):normalize())
                    local idx = math.floor((-angle +math.pi +(m._menu.step /2)) %(math.pi *2) /m._menu.step) +1
                    if m._menu.selected and m._menu.selected ~= idx then
                        m._menu[m._menu.selected]:animate{
                            scale = {
                                value = {x=0.7, y=0.7},
                                duration = 0.2
                            },
                            opacity = {
                                value = 128,
                                duration = 0.2
                            }
                        }
                    end
                    if m._menu.selected ~= idx and m._menu[idx] then
                        m._menu.selected = idx
                        m._menu[m._menu.selected]:animate{
                            scale = {
                                value = {x=1, y=1},
                                duration = 0.2
                            },
                            opacity = {
                                value = 256,
                                duration = 0.2
                            }
                        }
                    end
                elseif m._menu.selected then
                    m._menu[m._menu.selected]:animate{
                        scale = {
                            value = {x=0.7, y=0.7},
                            duration = 0.2
                        },
                        opacity = {
                            value = 128,
                            duration = 0.2
                        }
                    }
                    m._menu.selected = nil
                end
            end
        end
        
        --]]
        
        -- MARK: Health regen
        
        if m.next_regen and time -m.next_regen >= 0 then
            m.object:set_hp(m.object:get_hp() +1)
        end
        
        m.ctl = ctl
        m.yaw = yaw
        m.pitch = pitch
        m.dir = dir
    end,
    set_character = function(m, to)
        m.character = to
        m.meta:set_string("character", to)
        if to == "vix" then
            if m.color_hud then
                m.object:hud_remove(m.color_hud)
                m.color_hud = nil
            end
        else
            if m.color and not m.color_hud then
                m.color_hud = m.object:hud_add {
                    type = "image",
                    position = {x=0.5,y=1},
                    offset = {x=0,y=0},
                    alignment = {x=0,y=-1},
                    scale = {x=500,y=5},
                    text = "[fill:1x1:0,0:"..artifact.colors[m.color],
                }
            end
        end
    end,
    set_color = function(m, color)
        if artifact.debug or artifact.story.get_state() > artifact.story.states.pre_vix and m.character == "key" then
            m.color = color
            m.meta:set_string("color", color or "")
            if m.color_hud then
                m.object:hud_remove(m.color_hud)
            end
            -- If we cleared the color by passing nil, there's no need to animate or re-add the HUD.
            if not color then return end
            m.color_hud = m.object:hud_add {
                type = "image",
                position = {x=0.5,y=1},
                offset = {x=0,y=0},
                alignment = {x=0,y=-1},
                scale = {x=500,y=5},
                text = "[fill:1x1:0,0:"..artifact.colors[m.color],
            }
            local el = artifact.hud_add(m, {
                type = "image",
                pos = {x=0.5,y=0.5},
                scale = {x=10000,y=10000},
                opacity = 0,
                image = "[fill:1x1:"..artifact.colors[m.color]
            })
            el:animate {
                opacity = {
                    value = 25,
                    duration = 0.3
                }
            }
            minetest.after(0.3, function()
                el:animate {
                    opacity = {
                        value = 0,
                        duration = 0.3
                    }
                }
                minetest.after(0.3, function()
                    el:remove(m)
                end)
            end)
        end
    end,
    add_health_bar = function(m)
        m.healthbar = m.object:hud_add {
            type = "statbar",
            position = {x=0.5,y=1},
            offset = {x=-27 *5,y=artifact.debug and -96 or -42},
            scale = {x=4,y=4},
            alignment = {x=-1, y=-1},
            size = {x=27,y=27},
            text = m.character == "vix" and "artifact_heart_vix.png" or "artifact_heart.png",
            text2 = "artifact_heart_bg.png",
            number = 20,
            item = 20,
        }
    end,
    set_hotbar_size = function(m, slots)
        local p = m.object
        p:hud_set_hotbar_itemcount(slots)
        local list = ""
        for i = 0, slots do
            list = list..":"..(21*i)..",0=artifact_hotbar_bg.png"
        end
        p:hud_set_hotbar_image("[combine:"..(21 *slots +1).."x22"..list)
        p:hud_set_hotbar_selected_image("artifact_hotbar_selected_bg.png")
    end,
    set_spawnpoint = function(m, pos)
        m.spawn_point = pos
        m.meta:set_string("spawnpoint", pos:to_string())
    end
}, {
    __call = function(_, ...)
        return Player.new(...)
    end
})

-- Skip the death screen. Once we have an actual damage
-- system, this can be customized properly.
function minetest.show_death_screen(p, reason)
    p:respawn()
end

-- Override respawning, so we can save progress.
minetest.register_on_respawnplayer(function(p)
    local m = artifact.players[p:get_player_name()]
    return true
    --[[ Disabled due to current lack of purpose.
    if m.spawn_point then
        p:set_pos(m.spawn_point)
        return true
    end
    --]]
end)

-- Mirror the player's HP in our custom HUD.
-- (We need a custom HUD so that we can change its appearance dynamically.)
minetest.register_on_player_hpchange(function(p, delta)
    local m = artifact.players[p:get_player_name()]
    local hp = p:get_hp() +delta
    if m.healthbar then
        p:hud_change(m.healthbar, "number", hp)
    end
    if hp < 20 then
        m.next_regen = minetest.get_us_time() +5000000
    else
        m.next_regen = nil
    end
end)

local _hand = minetest.registered_items[""]

function artifact.register_input(name)
    artifact.register_node("input_"..name, {
        inventory_image = "artifact_rmb_100.png",
        description = "",
        paramtype = "light",
        drawtype = "mesh",
        mesh = name == "key" and "artifact_hand_key.gltf" or "artifact_hand.gltf",
        tiles = name == "key" and {"artifact_blackrod.png"} or {"artifact_"..name..".png", "artifact_blackrod.png"},
        use_texture_alpha = "opaque",
        visual_scale = 1,
        wield_scale = vector.new(2,2,2),
        node_placement_prediction = "",
        on_construct = function(pos)
            minetest.remove_node(pos)
        end,
        drop = "",
        range = 0,
        pointabilities = {
            nodes = {
                ["group:everything"] = false
            },
            objects = {
                ["group:immortal"] = false,
                ["group:fleshy"] = false
            }
        },
        on_drop = function(s, p, pos)
            local m = artifact.players[p:get_player_name()]
            if not m._swapping_character and (artifact.debug or artifact.story.get_state() > artifact.story.states.pre_vix) then
                artifact.swap_character(m)
            end
            return s
        end,
        on_use = function(s, p)
            local m = artifact.players[p:get_player_name()]
            if m._grabbed_item then return end
            if m.pointed_obj and m.pointed_obj._grabbable then
                artifact.grab_device(m, m.pointed_obj)
                return
            end
            if m.character == "vix" then
                artifact.do_shoot(m)
            else
                artifact.do_whack(m)
            end
        end
    })
end
artifact.register_input "key"
artifact.register_input "vix"

-- Apparently the hand range is applied very briefly when switching items.
if not artifact.debug then
    minetest.override_item("", {range = 0})
end


minetest.register_globalstep(function()
    for _, m in pairs(artifact.players) do
        m:tick()
    end
end)


minetest.register_on_joinplayer(function(p)
    artifact.players[p:get_player_name()] = Player(p)
    if artifact.debug then
        -- Make sure we don't have to `/grantme` a million times while testing.
        minetest.registered_chatcommands.grantme.func(p:get_player_name(), "all")
    end
end)

-- Imposters will be kicked. (But why would you run this on a server anyway?)
minetest.register_on_prejoinplayer(function(name)
    if name == "Key" or name == "Vix" then
        return "That name is already taken by one of the characters!"
    end
end)

minetest.register_on_leaveplayer(function(p)
    artifact.players[p:get_player_name()] = nil
end)
