-- You can edit this function to add support for your favorite notification system
function SimpleNotify(message)
    if Config.NotificationsAsChatMessage then
        TriggerEvent("chat:addMessage", { color = { 255, 255, 255 }, args = { tostring(message) } })
    else
        BeginTextCommandThefeedPost("STRING")
        AddTextComponentSubstringPlayerName(message)
        EndTextCommandThefeedPostTicker(true, true)
    end
end

-- Don't touch after this line if you don't know what you're doing
CreateExport = function(name, func)
    AddEventHandler('__cfx_export_rpemotes_'..name, function(setCb)
        setCb(function(...)
            return func(...)
        end)
    end)
    exports(name, func)
end

function DebugPrint(...)
    if Config.DebugDisplay then
        print(...)
    end
end

function FirstToUpper(str)
    return (str:gsub("^%l", string.upper))
end

function IsPlayerAiming(player)
    return (IsPlayerFreeAiming(player) or IsAimCamActive() or IsAimCamThirdPersonActive()) and
    tonumber(GetSelectedPedWeapon(player)) ~= tonumber(GetHashKey("WEAPON_UNARMED"))
end

function CanPlayerCrouchCrawl(playerPed)
    return IsPedOnFoot(playerPed)
        and not IsPedJumping(playerPed)
        and not IsPedFalling(playerPed)
        and not IsPedInjured(playerPed)
        and not IsPedInMeleeCombat(playerPed)
        and not IsPedRagdoll(playerPed)
end

function PlayAnimOnce(playerPed, animDict, animName, blendInSpeed, blendOutSpeed, duration, startTime)
    LoadAnim(animDict)
    TaskPlayAnim(playerPed, animDict, animName, blendInSpeed or 2.0, blendOutSpeed or 2.0, duration or -1, 0,
        startTime or 0.0, false, false, false)
    RemoveAnimDict(animDict)
end

function ChangeHeadingSmooth(playerPed, amount, time)
    local times = math.abs(amount)
    local test = amount / times
    local wait = time / times

    for _i = 1, times do
        Wait(wait)
        SetEntityHeading(playerPed, GetEntityHeading(playerPed) + test)
    end
end

function EmoteChatMessage(msg, multiline)
    if msg then
        TriggerEvent("chat:addMessage", {
            multiline = multiline == true or false,
            color = { 255, 255, 255 },
            args = { "^1Help^0", tostring(msg) }
        })
    end
end

function PairsByKeys(t, f)
    local a = {}
    for n in pairs(t) do
        a[#a + 1] = n
    end
    table.sort(a, f)
    local i = 0             -- iterator variable
    local iter = function() -- iterator function
        i = i + 1
        if a[i] == nil then
            return nil
        else
            return a[i], t[a[i]]
        end
    end
    return iter
end

function LoadAnim(dict)
    if not DoesAnimDictExist(dict) then
        return false
    end

    local timeout = 2000
    while not HasAnimDictLoaded(dict) and timeout > 0 do
        RequestAnimDict(dict)
        Wait(5)
        timeout = timeout - 5
    end
    if timeout == 0 then
        DebugPrint("Loading anim dict " .. dict .. " timed out")
        return false
    else
        return true
    end
end

function LoadPropDict(model)
    if not HasModelLoaded(GetHashKey(model)) then
        RequestModel(GetHashKey(model))
        local timeout = 2000
        while not HasModelLoaded(GetHashKey(model)) and timeout > 0 do
            Wait(5)
            timeout = timeout - 5
        end
        if timeout == 0 then
            DebugPrint("Loading model " .. model .. " timed out")
            return
        end
    end
end

function TableHasKey(table, key)
    return table[key] ~= nil
end

function RequestWalking(set)
    local timeout = GetGameTimer() + 5000
    while not HasAnimSetLoaded(set) and GetGameTimer() < timeout do
        RequestAnimSet(set)
        Wait(5)
    end
end

function GetPedInFront()
    local player = PlayerId()
    local plyPed = GetPlayerPed(player)
    local plyPos = GetEntityCoords(plyPed, false)
    local plyOffset = GetOffsetFromEntityInWorldCoords(plyPed, 0.0, 1.3, 0.0)
    local rayHandle = StartShapeTestCapsule(plyPos.x, plyPos.y, plyPos.z, plyOffset.x, plyOffset.y, plyOffset.z, 10.0, 12
    , plyPed, 7)
    local _, _, _, _, ped2 = GetShapeTestResult(rayHandle)
    return ped2
end

function NearbysOnCommand(source, args, raw)
    local NearbysCommand = ""
    for a, b in PairsByKeys(EmoteData) do
        if type(b) == "table" and b.emoteType == EmoteType.SHARED then
            NearbysCommand = NearbysCommand .. a .. ", "
        end
    end
    EmoteChatMessage(NearbysCommand)
    EmoteChatMessage(Translate('emotemenucmd'))
end

function GetClosestPlayer()
    local players = GetPlayers()
    local closestDistance = -1
    local closestPlayer
    local ped = PlayerPedId()
    local pedCoords = GetEntityCoords(ped, false)

    for index, value in ipairs(players) do
        local target = GetPlayerPed(value)
        if (target ~= ped) then
            local targetCoords = GetEntityCoords(GetPlayerPed(value), false)
            local distance = GetDistanceBetweenCoords(targetCoords["x"], targetCoords["y"], targetCoords["z"],
                pedCoords["x"], pedCoords["y"], pedCoords["z"], true)
            if (closestDistance == -1 or closestDistance > distance) then
                closestPlayer = value
                closestDistance = distance
            end
        end
    end
    return closestPlayer, closestDistance
end

function GetPlayers()
    local players = {}

    for i = 0, 255 do
        if NetworkIsPlayerActive(i) then
            players[#players + 1] = i
        end
    end

    return players
end

-- Function that'll check if player is already proning, using news cam or else

---@param ignores? table key string is the ignored value
function IsInActionWithErrorMessage(ignores)
    if ignores then DebugPrint(ignores) end
    DebugPrint('IsProne', IsProne)
    DebugPrint('IsUsingNewscam', IsUsingNewscam)
    DebugPrint('IsUsingBinoculars', IsUsingBinoculars)
    if ignores == nil then ignores = {} end

    if not ignores.IsProne and IsProne then
        EmoteChatMessage(Translate('no_anim_crawling'))
        return true
    end
    if not ignores.IsUsingNewscam and IsUsingNewscam then
        -- TODO: use specific error message
        EmoteChatMessage(Translate('no_anim_right_now'))
        return true
    end
    if not ignores.IsUsingBinoculars and IsUsingBinoculars then
        -- TODO: use specific error message
        EmoteChatMessage(Translate('no_anim_right_now'))
        return true
    end

    return false
end

function HideHUDThisFrame()
    HideHelpTextThisFrame()
    HideHudAndRadarThisFrame()
    HideHudComponentThisFrame(19) -- weapon wheel
    HideHudComponentThisFrame(1)  -- Wanted Stars
    HideHudComponentThisFrame(2)  -- Weapon icon
    HideHudComponentThisFrame(3)  -- Cash
    HideHudComponentThisFrame(4)  -- MP CASH
    HideHudComponentThisFrame(13) -- Cash Change
    HideHudComponentThisFrame(11) -- Floating Help Text
    HideHudComponentThisFrame(12) -- more floating help text
    HideHudComponentThisFrame(15) -- Subtitle Text
    HideHudComponentThisFrame(18) -- Game Stream
end

function SetupButtons(button)
    local scaleform = RequestScaleformMovie("instructional_buttons")
    while not HasScaleformMovieLoaded(scaleform) do
        Wait(10)
    end
    PushScaleformMovieFunction(scaleform, "CLEAR_ALL")
    PopScaleformMovieFunctionVoid()

    PushScaleformMovieFunction(scaleform, "SET_CLEAR_SPACE")
    PushScaleformMovieFunctionParameterInt(200)
    PopScaleformMovieFunctionVoid()

    for i, btn in pairs(button) do
        PushScaleformMovieFunction(scaleform, "SET_DATA_SLOT")
        PushScaleformMovieFunctionParameterInt(i - 1)
        ScaleformMovieMethodAddParamPlayerNameString(GetControlInstructionalButton(0, btn.key, true))
        BeginTextCommandScaleformString("STRING")
        AddTextComponentScaleform(Translate(btn.text))
        EndTextCommandScaleformString()
        PopScaleformMovieFunctionVoid()
    end

    PushScaleformMovieFunction(scaleform, "DRAW_INSTRUCTIONAL_BUTTONS")
    PopScaleformMovieFunctionVoid()

    return scaleform
end

function HandleZoomAndCheckRotation(cam, fov)
    local zoomspeed = 10.0 -- camera zoom speed
    local lPed = PlayerPedId()

    local fov_max = 70.0
    local fov_min = 10.0 -- max zoom level (smaller fov is more zoom)
    local speed_lr = 8.0 -- speed by which the camera pans left-right
    local speed_ud = 8.0 -- speed by which the camera pans up-down

    local zoomvalue = (1.0 / (fov_max - fov_min)) * (fov - fov_min)
    local rightAxisX = GetDisabledControlNormal(0, 220)
    local rightAxisY = GetDisabledControlNormal(0, 221)
    local rotation = GetCamRot(cam, 2)

    if rightAxisX ~= 0.0 or rightAxisY ~= 0.0 then
        local new_z = rotation.z + rightAxisX * -1.0 * (speed_ud) * (zoomvalue + 0.1)
        local new_x = math.max(math.min(20.0, rotation.x + rightAxisY * -1.0 * (speed_lr) * (zoomvalue + 0.1)), -29.5)
        SetCamRot(cam, new_x, 0.0, new_z, 2)
    end

    if not (IsPedSittingInAnyVehicle(lPed)) then
        if IsControlJustPressed(0, 241) then -- Scrollup
            fov = math.max(fov - zoomspeed, fov_min)
        end
        if IsControlJustPressed(0, 242) then
            fov = math.min(fov + zoomspeed, fov_max) -- ScrollDown
        end
        local current_fov = GetCamFov(cam)
        if math.abs(fov - current_fov) < 0.1 then
            fov = current_fov
        end
        SetCamFov(cam, current_fov + (fov - current_fov) * 0.05)
    else
        if IsControlJustPressed(0, 17) then -- Scrollup
            fov = math.max(fov - zoomspeed, fov_min)
        end
        if IsControlJustPressed(0, 16) then
            fov = math.min(fov + zoomspeed, fov_max) -- ScrollDown
        end
        local current_fov = GetCamFov(cam)
        if math.abs(fov - current_fov) < 0.1 then -- the difference is too small, just set the value directly to avoid unneeded updates to FOV of order 10^-5
            fov = current_fov
        end
        SetCamFov(cam, current_fov + (fov - current_fov) * 0.05) -- Smoothing of camera zoom
    end

    return fov
end

----------------------------------------------------------------------

ShowPed = false

function ShowPedMenu(zoom)
    if not Config.PreviewPed then return end

    if not ShowPed then
        CreateThread(function()
            local playerPed = PlayerPedId()
            local coords = GetEntityCoords(playerPed) - vector3(0.0, 0.0, 10.0)
            ClonedPed = CreatePed(26, GetEntityModel(playerPed), coords.x, coords.y, coords.z, 0, false, false)
            ClonePedToTarget(playerPed, ClonedPed)

            SetEntityInvincible(ClonedPed, true)
            SetEntityLocallyVisible(ClonedPed)
            NetworkSetEntityInvisibleToNetwork(ClonedPed, true)
            SetEntityCanBeDamaged(ClonedPed, false)
            SetBlockingOfNonTemporaryEvents(ClonedPed, true)
            SetEntityAlpha(ClonedPed, 254, false)
            SetEntityCollision(ClonedPed, false, false)

            ShowPed = true

            local positionBuffer = {}
            local bufferSize = 5

            while ShowPed do
                local screencoordsX = zoom and 0.6 or 0.65135417461395
                local screencoordsY = zoom and 1.9 or 0.77

                if Config.MenuPosition == "left" then
                    screencoordsX = 1.0 - screencoordsX
                end

                local world, normal = GetWorldCoordFromScreenCoord(screencoordsX, screencoordsY)
                local depth = zoom and 2.0 or 3.5
                local target = world + normal * depth
                local camRot = GetGameplayCamRot(2)

                positionBuffer[#positionBuffer + 1] = target
                if #positionBuffer > bufferSize then
                    table.remove(positionBuffer, 1)
                end

                local averagedTarget = vector3(0, 0, 0)
                for _, position in ipairs(positionBuffer) do
                    averagedTarget = averagedTarget + position
                end
                averagedTarget = averagedTarget / #positionBuffer

                SetEntityCoords(ClonedPed, averagedTarget.x, averagedTarget.y, averagedTarget.z, false, false, false, true)
                local heading_offset = Config.MenuPosition == "left" and 170.0 or 190.0
                SetEntityHeading(ClonedPed, camRot.z + heading_offset)
                SetEntityRotation(ClonedPed, camRot.x * (-1), 0.0, camRot.z + 170.0, 2, false)

                Wait(4)
            end

            DeleteEntity(ClonedPed)
            ClonedPed = nil
        end)
    end
end

function ClosePedMenu()
    if not Config.PreviewPed then return end

    if ClonedPed then
        ShowPed = false
        ClearPedTaskPreview()
        DeleteEntity(ClonedPed)
    end
end

function ClearPedTaskPreview()
    if not Config.PreviewPed then return end

    if ClonedPed then
        DestroyAllProps(true)
        ClearPedTasksImmediately(ClonedPed)
    end
end
