local mod_name = minetest.get_current_modname()
local mod_path = minetest.get_modpath(mod_name)
local S = minetest.get_translator(mod_name)

function pmb_wings.get_velocity_from_gradient(vel, look_dir)
end

local pl = {}
function pmb_wings.check_player(player)
    if not pl[player] then
        pl[player] = {
            last_pos = player:get_pos(),
            last_speed = 0,
            last_ctrl = {},
        }
    end
end


-- for gamemodes
local _gm = minetest.get_modpath("pmb_gamemodes") ~= nil
if _gm then
    pmb_gamemodes.add_gamemode_tags("survival", {
        wings_flight = true,
        wings_takeoff = true
    })
    pmb_gamemodes.add_gamemode_tags("creative", {
        wings_flight = true,
        wings_takeoff = true
    })
end
local function has_perms(player, tag)
    if not _gm then return end
    return pmb_gamemodes.player_has_tag(player, tag)
end


local wings_default_params = {
    max_speed = 60, -- max speed to allow
    gravity = 0.7, -- gravity effect to add to the player
    max_acceleration = 6, -- max speed to add per second
    deceleration = 0.021, -- constant multiplicative reduction in speed ==> speed * (1-n)
    auto_pitch_up = 0.3, -- pitches up glide direction

    fall_bonus = 0.6, -- how much accel per pos.y fallen
    climb_penalty = 0.1, -- how much to slow down per pos.y climbed
    -- lift_factor = 0.2, -- smaller num ==> less counteracting gravity due to speed
    lift_max = 0.9, -- max amount to use speed for reducing gravity
    lift_min = 0.4,
    start_mult_y = 3, -- for each vel.y
    start_mult_vel = 2, -- for each length(vel)
    low_speed_threshold = 40, -- if below this ==> get extra fall bonus
    low_speed_bonus = 1,

    pitch_acc = 0.0, -- how much accel per dir.y down
    pitch_acc_bonus = -0.1, -- how much accel per dir.y down
    pitch_up_penalty = 0.5, -- looking up slows you down
    lift_resistance = 0.94, -- resists Y velocity, but doesn't affect inertia
    yaw_deceleration = 0.35, -- per half rotation (pi)
    yaw_deceleration_threshold = 1.3, -- radians per sec, under this, yaw decel is x / 10
    slingshot_allow = true, -- allows sprint + rclick to slingshot the player
    -- slingshot_speed = 40, -- overrides speed after slingshot
    slingshot_velocity = 30, -- how much actuall vel to add when starting slingshot; how far up you fling
    slingshot_delay = 3, -- max amount of time before cancelling slingshot
    slingshot_vel_threshold = -8, -- vel.y < this --> start flying
    slingshot_direction = vector.new(1,0.3,1),

    hud = true, -- shows cooldown
    wind = true, -- plays sound
    fov = true, -- "speed effect"
    max_fov = 0.3, -- 1 + this
}


local dtimeavg = 0.1

local function lerp(a, b, f)
    return (
        a + ((b-a) * f)
    )
end

--[[
-------------------------
---------- HUD ----------
-------------------------
]]

-- remove cooldown hud elements from this player
local function remove_hud(player)
    local pi = pl[player]
    if not pi.hud_id_bg then
        return
    end
    player:hud_remove(pi.hud_id_bg)
    player:hud_remove(pi.hud_id_bar)
    pi.hud_id_bg = nil
    pi.hud_id_bar = nil
end


-- do wind sounds based on the item's def and player speed
local function handle_hud(itemstack, player, dtime)
    local pi = pl[player]
    local wear_factor = 1 - ((itemstack:get_wear()) / 65536)
    local scale = 3
    -- add the hud
    if (wear_factor ~= 1) and not pi.hud_id_bg then
        pi.hud_id_bg = player:hud_add({
            hud_elem_type = "image",
            alignment = {x=0.0, y=1.0},
            position = {x=0.5, y=0.89},
            name = "pmb_wings_hud_bg",
            text = "pmb_wings_hud_bg.png",
            z_index = 807,
            scale = {x = scale, y = scale},
            offset = {x = 0, y = 0},
        })
        pi.hud_id_bar = player:hud_add({
            hud_elem_type = "image",
            alignment = {x=1.0, y=1.0},
            position = {x=0.5, y=0.89},
            name = "pmb_wings_hud_bar",
            text = "pmb_wings_hud_bar.png",
            z_index = 808,
            scale = {x = scale, y = scale},
            offset = {x = (-32 +4)*scale, y = (2)*scale},
        })
    end

    -- doing normal update
    if pi.hud_id_bg and wear_factor ~= 1 then
        player:hud_change(pi.hud_id_bar, "scale", {x=wear_factor * scale * (64-8) + 1, y=scale})
        if pi.hud_fade then
            player:hud_change(pi.hud_id_bg, "text", "pmb_wings_hud_bg.png")
            player:hud_change(pi.hud_id_bar, "text", "pmb_wings_hud_bar.png")
        end
    end

    -- finished so will do fade now
    if pi.hud_id_bg and wear_factor == 1 and not pi.hud_fade then
        pi.hud_fade = 0.3
    end

    -- fading update
    if wear_factor == 1 and pi.hud_fade and pi.hud_fade > 0 then
        pi.hud_fade = pi.hud_fade - dtime
        player:hud_change(pi.hud_id_bg, "text", "pmb_wings_hud_bg.png^[opacity:"..(math.floor(pi.hud_fade * 25.5*2)*5))
        player:hud_change(pi.hud_id_bar, "text", "pmb_wings_hud_bar.png^[opacity:"..(math.floor(pi.hud_fade * 25.5*2)*5))
    end

    -- finished fading
    if pi.hud_id_bg and pi.hud_fade and pi.hud_fade <= 0 then
        pi.hud_fade = nil
        remove_hud(player)
    end
end

--[[
-------------------------
-------- SYSTEM ---------
-------------------------
]]

-- allow other mods / files access to the info about flight here
function pmb_wings.get_player_info(player)
    return pl[player]
end


-- these functions are used by items
function pmb_wings.on_unequipped(itemstack, player)
    itemstack = pmb_wings.to_walking(itemstack, player)
    remove_hud(player)
    pl[player] = nil
    return itemstack
end


function pmb_wings.on_equipped(itemstack, player, info)
    pmb_wings.check_player(player)
    itemstack = pmb_wings.to_walking(itemstack, player, info and info.is_from_joining)
    return itemstack
end


-- use raycast to test if on floor
local function get_is_on_floor(player)
    local pos = player:get_pos()
    local below = vector.offset(pos, 0, -0.2, 0)
    local ray = minetest.raycast(pos, below, false, false)
    for pointed_thing in ray do
        local ndef = minetest.registered_nodes[minetest.get_node(pointed_thing.under).name]
        if ndef and ndef.walkable then
            return true
        end
        return false
    end
    local ndef = minetest.registered_nodes[minetest.get_node(pos).name]
    if ndef and ndef.walkable then
        return true
    end
    return false
end


-- handle resetting everything to default
function pmb_wings.to_walking(itemstack, player, w, is_from_joining)
    playerphysics.remove_physics_factor(player, "gravity", "pmb_wings:internal")
    playerphysics.remove_physics_factor(player, "speed", "pmb_wings:internal")
    local pi = pl[player]
    if pi and pi._wind_sound then
        minetest.sound_fade(pi._wind_sound, 20, 0)
        pi._wind_sound = nil
        pi.ctrl = {}
        pi.flying = false
        pi.last_real_speed = nil
        pi.last_speed = nil
        pi.last_yaw = nil
    end
    pmb_util.unset_fov(player, "pmb_wings:fov")

    local idef = itemstack and itemstack:get_definition()
    if (not is_from_joining) and idef and idef._wings_on_land then
        itemstack = idef._wings_on_land(ItemStack(itemstack), player) or itemstack
    end
    -- minetest.log("walking")
    return itemstack
end


-- handle giving initial velocity and starting flight
function pmb_wings.to_flying(itemstack, player, gravity, w)
    w = (itemstack and itemstack:get_definition()._wings_params) or w
    local vel = player:get_velocity()
    if w.start_vel_cancel then
        local nvel = vector.new(
            vel.x * (w.start_vel_add or 0),
            vel.y * -w.start_vel_cancel,
            vel.z * (w.start_vel_add or 0)
        )
        player:add_velocity(nvel)
    end
    pl[player].last_speed = vector.length(vel) * (w.start_mult_vel or 1) - (vel.y + 2) * (w.start_mult_vel or 1)
    playerphysics.add_physics_factor(player, "gravity", "pmb_wings:internal", gravity or 0.2)
    playerphysics.add_physics_factor(player, "speed", "pmb_wings:internal", 0.2)

    local pi = pl[player]
    pi.flying = true
    pi.last_real_speed = nil
    pi.last_yaw = nil
    pi.last_pos = player:get_pos()

    local idef = (itemstack and itemstack:get_definition())
    if idef and idef._wings_on_fly then
        itemstack = idef._wings_on_fly(ItemStack(itemstack), player) or itemstack
    end
    -- minetest.log("flying")
    return itemstack
end


--[[
-------------------------
-------- EFFECTS --------
-------------------------
]]

-- make the screen have a speed FOV effect
local function handle_fov(w, player, dtime)
    if not minetest.get_modpath("pmb_util") then return end

    local speed = vector.length(player:get_velocity())
    local factor = (speed - 1) / (w.max_speed or 50)
    factor = math.min(1, math.max(0, factor))

    pmb_util.set_fov(player, {
        tag = "pmb_wings:fov",
        fov = 1 + factor * (w.max_fov or 0.3),
        is_multiplier = true,
        transition_time = 0.2,
    })
end


-- do wind sounds based on the item's def and player speed
local function handle_wind_sounds(w, player, dtime)
    local pi = pl[player]
    pi._wind_update = (pi._wind_update or 0) - dtime
    if pi._wind_update < 0 then pi._wind_update = pi._wind_update + 0.2
    else return end

    local speed = vector.length(player:get_velocity())
    local windfactor = (speed - 1) / (w.max_speed or 50)
    windfactor = math.min(1, math.max(0, windfactor))

    local gain = (windfactor - 0.1) * 0.3

    if not pi._wind_sound then
        pi._wind_sound = minetest.sound_play((w.sound_wind or "pmb_wings_wind"), {
            gain = 0.01,
            pitch = 1,
            to_player = player:get_player_name(),
            loop = true,
        })
    end
    minetest.sound_fade(pi._wind_sound, 0.8, math.max(0, gain))
    if pi._wind_sound and (gain <= 0) then pi._wind_sound = nil end
end


--[[
-------------------------
--- SLINGSHOT SYSTEM ----
-------------------------
]]

-- start the glide at the top of the fling
local function finish_slingshot(itemstack, player, dtime)
    local w = itemstack:get_definition()._wings_params
    local pi = pl[player]
    itemstack = pmb_wings.to_flying(itemstack, player, w.gravity)
    pi.flying = true
    pi.last_speed = w.slingshot_speed or pi.last_speed
    -- prevents checking ground
    pi.takeoff_grace = 1
    return itemstack
end


-- fling player up so they can glide
local function do_slingshot(itemstack, player, dtime)
    local w = itemstack:get_definition()._wings_params
    if pmb_combat.cooldown.get_mode(itemstack) == "" then
        itemstack = pmb_combat.cooldown.cooldown_start(itemstack, player)
        local pi = pl[player]

        if w.slingshot_velocity then
            local look_dir = player:get_look_dir()
            if w.slingshot_direction then
                look_dir.y = 0
                look_dir = vector.normalize(look_dir)
                look_dir.x = look_dir.x * w.slingshot_direction.x
                look_dir.z = look_dir.z * w.slingshot_direction.z
                look_dir.y = w.slingshot_direction.y
            else
                look_dir.y = (look_dir.y * 0.1) + 2
            end
            look_dir = vector.normalize(look_dir)
            player:add_velocity(look_dir * (w.slingshot_velocity))
            playerphysics.add_physics_factor(player, "speed", "pmb_wings:internal", 0.2)
        end
        pi.slingshot_delay = (w.slingshot_delay or 1.5)
    end
    return itemstack
end

--[[
-------------------------
--------- MAIN ----------
-------------------------
]]

local function anglediff(a0, a1)
    local max = math.pi * 2
    local da = (a1 - a0) % max
    return 2 * da % max - da
end

function pmb_wings.get_glide_velocity(w, player, dtime)
    pmb_wings.check_player(player)
    local pi = pl[player]

    local vel = player:get_velocity()

    local look_dir = player:get_look_dir()
    local pos = player:get_pos()
    -- local real_speed = vector.length(vel) -- not implemented yet: slow down on collision

    local fall = pos.y - pi.last_pos.y

    local add_speed = pi.last_speed
    -------------------
    -- filters
    if w.fall_bonus and (fall < 0) then
        add_speed = add_speed - fall * w.fall_bonus
    end
    if w.low_speed_bonus and add_speed < (w.low_speed_threshold or 20) and (fall < 0) then
        local factor = 1
        add_speed = add_speed - fall * w.low_speed_bonus * factor
    end
    if w.climb_penalty and (fall > 0) then
        add_speed = add_speed - fall * w.climb_penalty
    end
    if w.pitch_acc then
        local pitch = look_dir.y - (w.pitch_acc_bonus or 0)
        if pitch < 0 then
            add_speed = add_speed + w.pitch_acc * -pitch
        end
    end
    if w.pitch_up_penalty then
        local pitch = look_dir.y - (w.pitch_acc_bonus or 0)
        if pitch > 0 then
            add_speed = add_speed + w.pitch_up_penalty * -pitch
        end
    end
    if w.yaw_deceleration then
        local yaw = minetest.dir_to_yaw(look_dir)
        local diff = anglediff(yaw, pi.last_yaw or yaw)
        local factor = (diff / 3.14) * w.yaw_deceleration
        if diff < w.yaw_deceleration_threshold * dtimeavg then
            factor = factor * 0.02
        end
        add_speed = add_speed * (1-factor)
        pi.last_yaw = yaw
    end
    -- don't accelerate too fast
    if w.max_acceleration then
        if add_speed - pi.last_speed > w.max_acceleration then
            add_speed = pi.last_speed + w.max_acceleration
        end
    end
    -------------------
    add_speed = math.min(w.max_speed, add_speed)
    add_speed = math.max(0, add_speed)


    -- test for collision using heuristics
    local len = vector.length(vel)
    if len - (pi.last_real_speed or len) > w.collision_threshold then
        if vel.x == 0 or vel.y == 0 or vel.z == 0 then
            add_speed = add_speed * w.collision_factor
        end
    end
    pi.last_real_speed = len


    pi.last_speed = add_speed * (1 - (w.deceleration or 0) * dtimeavg)

    if w.pitch_clamp and look_dir.y > w.pitch_clamp then
        look_dir.y = w.pitch_clamp + (look_dir.y - w.pitch_clamp) * 0.1
    end

    local glide_dir = vector.new(
        look_dir.x,
        look_dir.y + w.auto_pitch_up,
        look_dir.z
    )

    local add_vel = glide_dir * (add_speed * dtimeavg)

    local take_vel = vel * -dtimeavg

    if w.lift_factor then
        local speed_p = (add_speed / (w.max_speed or 100)) * (w.lift_factor or 0)
        speed_p = math.max(w.lift_min or 0.2, math.min(w.lift_max or 0.9, speed_p))
        take_vel.y = take_vel.y * speed_p
    end

    add_vel = add_vel + take_vel

    local threshold = w.lift_resistance_threshold or 1
    if w.lift_resistance and (add_vel.y) > threshold then
        add_vel.y = threshold + (add_vel.y - threshold) * (1-w.lift_resistance)
    end

    pi.last_pos = pos
    return add_vel
end


-- items will use this in their on_step
function pmb_wings.on_equipment_step(itemstack, player, dtime)
    if not player then return end
    if not has_perms(player, "wings_flight") then return end
    pmb_wings.check_player(player)
    local pi = pl[player]

    local idef = itemstack:get_definition()
    if not idef then return end -- if no itemstack, just give up

    local w = idef._wings_params or wings_default_params

    local vel = player:get_velocity()

    -- test if should be flying or walking
    local is_on_floor = get_is_on_floor(player)
    local is_in_water
    local node = minetest.get_node(player:get_pos())
    is_in_water = minetest.get_item_group(node.name, "liquid") > 0
    local ctrl = player:get_player_control() or {}

    -- handle starting and stopping flight, slingshot etc.
    if not pi.takeoff_grace then
        if (vel.y < 0) and (not pi.flying) and (not is_on_floor)
        and (not pi.last_ctrl.jump) and ctrl and ctrl.jump then
            pi.flying = true
            itemstack = pmb_wings.to_flying(itemstack, player, w.gravity)
        elseif pi.flying and (is_on_floor or is_in_water) then
            pi.flying = false
            itemstack = pmb_wings.to_walking(itemstack, player)
        elseif is_on_floor and w.slingshot_allow
        and ctrl.up and (ctrl.place and not pi.last_ctrl.place) and ctrl.aux1
        and (vector.length(vel) < 10) and has_perms(player, "wings_takeoff") then
            itemstack = do_slingshot(ItemStack(itemstack), player, dtime)
        elseif (not pi.flying) and (pi.slingshot_delay ~= nil)
        and (vel.y < (w.slingshot_vel_threshold or -5)) then
            pi.slingshot_delay = nil
            itemstack = finish_slingshot(ItemStack(itemstack), player, dtime)
        end
    end

    -- don't go back to walking so soon after starting flight
    if pi.takeoff_grace and pi.takeoff_grace > 0 then
        pi.takeoff_grace = pi.takeoff_grace - dtimeavg
        if pi.takeoff_grace < 0 then pi.takeoff_grace = nil end
    end

    -- max amount of time to wait to fall back again and activate slingshot effect
    if pi.slingshot_delay and pi.slingshot_delay > 0 then
        pi.slingshot_delay = pi.slingshot_delay - dtimeavg
        if pi.slingshot_delay < 0 then pi.slingshot_delay = nil end
    end

    -- keep track of last controls so we can use them to determine "I just pressed jump"
    pi.last_ctrl = ctrl

    -- handle hud elements for when the wings are on cooldown from slingshot effect, even when not flying
    if w.hud then
        handle_hud(itemstack, player, dtime)
    end

    -- only do physics and effects when flying
    if not pi.flying then return itemstack end

    -- handle wind, fov and other effects
    if w.wind then
        handle_wind_sounds(w, player, dtime)
    end
    if w.fov then
        handle_fov(w, player, dtime)
    end

    local add_vel = pmb_wings.get_glide_velocity(w, player, dtime)
    player:add_velocity(add_vel)

    -- so that you can modify the itemstack if need be
    return itemstack
end



minetest.register_globalstep(function(dtime)
    dtimeavg = (dtimeavg * 9 + dtime) * 0.1
end)


