#include "umf.lua"
local config = OptionsKeys {
    legacy = {"boolean", false},
    hybrid = {"boolean", true},
    noslowmo = {"boolean", false},
    clip = {"boolean", false},
    key = {
        fly = {"string", "v"},
        forward = {"string", "up"},
        backward = {"string", "down"},
        left = {"string", "left"},
        right = {"string", "right"},
        up = {"string", "space"},
        down = "string",
        slow = {"string", "ctrl"},
        fast = {"string", "shift"}
    }
}

if not SetCameraFov or config.legacy then

    function init()
        FlyActive = false
        Target = nil
        NoclipActive = false
        NoclipTransform = nil
        NoclipAngle = nil
    end

    local function toEuler(quat)
        local x, y, z, w = quat[1], quat[2], quat[3], quat[4]
        -- Credit to https://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToEuler/index.htm

        local bank, heading, attitude

        local s = 2 * x * y + 2 * z * w
        if s >= 1 then
            heading = 2 * math.atan2(x, w)
            bank = 0
            attitude = math.pi / 2
        elseif s <= -1 then
            heading = -2 * math.atan2(x, w)
            bank = 0
            attitude = math.pi / -2
        else
            bank = math.atan2(2 * x * w - 2 * y * z, 1 - 2 * x ^ 2 - 2 * z ^ 2)
            heading = math.atan2(2 * y * w - 2 * x * z,
                                 1 - 2 * y ^ 2 - 2 * z ^ 2)
            attitude = math.asin(s)
        end

        return math.deg(bank), math.deg(heading), math.deg(attitude)
    end

    local function aorb(a, b, d) return (a and d or 0) - (b and d or 0) end

    local function getDelta(dt, tr)
        local f, b, l, r, s, c = InputDown(config.key.forward),
                                 InputDown(config.key.backward),
                                 InputDown(config.key.left),
                                 InputDown(config.key.right),
                                 InputDown(config.key.up),
                                 InputDown(config.key.down)
        if f or b or l or r or s or c then
            local dist = InputDown(config.key.slow) and 0.01 or
                             InputDown(config.key.fast) and 0.3 or 0.1
            local fup = aorb(s, c, dist)
            return VecAdd(Vec(0, (f and 0 or fup) + dt / 10, 0),
                          TransformToParentVec(tr, Vec(aorb(r, l, dist),
                                                       (f and fup or 0),
                                                       aorb(b, f, dist))))
        end
    end

    function tick(dt)
        if InputPressed(config.key.fly) then
            FlyActive = not FlyActive
            Target = nil
            NoclipActive = false
        end
        if not FlyActive then return end

        local height = InputDown("ctrl") and 0.85 or 1.7
        if NoclipActive then
            if not Target then Target = NoclipTransform.pos end
            local dx, dy = InputValue("mousedx"), InputValue("mousedy")
            local sensitivity = GetInt("options.input.sensitivity") / 100
            NoclipAngle[1] = math.min(math.max(
                                          NoclipAngle[1] - dy / 10 * sensitivity,
                                          -80), 80)
            NoclipAngle[2] = NoclipAngle[2] - dx / 10 * sensitivity
            NoclipTransform.rot = QuatEuler(NoclipAngle[1], NoclipAngle[2], 0)
            local d = getDelta(0, NoclipTransform)
            if d then
                Target = VecAdd(Target, d)
                NoclipTransform.pos = Target
            end
            SetCameraTransform(Transform(
                                   VecAdd(NoclipTransform.pos, {0, height, 0}),
                                   NoclipTransform.rot))
            ;(SetPlayerTransformWithPitch or SetPlayerTransform)(NoclipTransform, true)
        else
            ---[[
            local current_pos = GetPlayerTransform().pos
            local v = GetPlayerVelocity()
            if not Target or VecLength(v) > 1 then
                Target = current_pos
            end

            local d = getDelta(dt, GetCameraTransform())
            if d then Target = VecAdd(Target, d) end
            SetPlayerVelocity(VecScale(VecSub(Target, current_pos), 100))
            -- ]]
            --[[
			local current_pos = GetPlayerTransform().pos
			if not Target then Target = current_pos end
			local d = getDelta(0, GetCameraTransform())
			if d then Target = VecAdd(Target, VecScale(d, 60 * dt)) end
			SetPlayerVelocity(VecScale(VecSub(Target, current_pos), 2 / dt))
			--]]
        end

        local nocliphit = config.hybrid and
                              (QueryRaycast(VecAdd(Target, {0, 0.3, 0}),
                                            {0, 1, 0}, height - .3, 0.2) or
                                  QueryRaycast(VecAdd(Target, {0, height, 0}),
                                               {0, -1, 0}, height - .3, 0.2))
        if nocliphit ~= NoclipActive then
            if NoclipActive then
                NoclipTransform = nil
                NoclipAngle = nil
            else
                NoclipTransform = GetPlayerTransform()
                NoclipAngle = Vec(toEuler(GetCameraTransform().rot))
            end
            NoclipActive = not NoclipActive
        end
    end

else

    function init()
        FlyActive = false

        MinFrameTime = config.noslowmo and 1 / 60 or 0
    end

    local function aorb(a, b, d) return (a and d or 0) - (b and d or 0) end

    local function getDelta(tr)
        local f, b, l, r, s, c = InputDown(config.key.forward),
                                 InputDown(config.key.backward),
                                 InputDown(config.key.left),
                                 InputDown(config.key.right),
                                 InputDown(config.key.up),
                                 InputDown(config.key.down)
        if f or b or l or r or s or c then
            local dist = InputDown(config.key.slow) and 0.02 or
                             InputDown(config.key.fast) and 0.5 or 0.2
            local fup = aorb(s, c, dist)
            local dup = VecScale(GetPlayerUp(), f and 0 or fup)
            return VecAdd(dup, --
            TransformToParentVec(tr, Vec(aorb(r, l, dist), (f and fup or 0),
                                         aorb(b, f, dist))))
        end
    end

    local function updatePosition(dt)
        if not FlyActive then return end
        local ct = GetPlayerTransform(true)

        if not FlyTransform then FlyTransform = ct end
        local d = getDelta(ct)
        if GetPlayerOrientation then
            local pitchyaw = QuatEuler(GetPlayerPitch(), GetPlayerYaw(), 0)
            local o = GetPlayerOrientation()
            ct.rot = pitchyaw -- Orientation hack (only works in third person??)
        end
        FlyTransform.rot = ct.rot
        if d then
            FlyTransform.pos = VecAdd(FlyTransform.pos, VecScale(d, 60 * dt))
        end
        if dt > 0 and VecLength(VecSub(FlyTransform.pos, ct.pos)) > 120 * dt then
            FlyTransform = ct
        end
        ;(SetPlayerTransformWithPitch or SetPlayerTransform)(FlyTransform, true)
        LastV = d
    end

    local timescale = 1
    function tick(dt)
        timescale = dt * GetFps() -- ...why is there no official way to do this
        if InputPressed(config.key.fly) then
            FlyActive = not FlyActive
            if not config.clip then
                SetPlayerParam("FlyMode", FlyActive)
            end
            FlyTransform = nil
            if not FlyActive and LastV then
                SetPlayerVelocity(VecScale(LastV, 60))
            end
        end
        if config.clip then
            dt = math.max(dt, MinFrameTime)
            updatePosition(dt)
        else
            updatePosition(0)
        end
    end

    function update(dt)
        if not config.clip then
            if config.noslowmo then
                dt = dt / timescale
            end
            updatePosition(dt)
        end
    end
end
