--[[---------------------------------------------------------------------------
Utility functions
---------------------------------------------------------------------------]]

local vector = FindMetaTable("Vector")
local meta = FindMetaTable("Player")

--[[---------------------------------------------------------------------------
Decides whether the vector could be seen by the player if they were to look at it
---------------------------------------------------------------------------]]
function vector:isInSight(filter, ply)
    ply = ply or LocalPlayer()
    local trace = {}
    trace.start = ply:EyePos()
    trace.endpos = self
    trace.filter = filter
    trace.mask = -1
    local TheTrace = util.TraceLine(trace)

    return not TheTrace.Hit, TheTrace.HitPos
end

--[[---------------------------------------------------------------------------
Turn a money amount into a pretty string
---------------------------------------------------------------------------]]
local function attachCurrency(str)
    local config = GAMEMODE.Config
    return config.currencyLeft and config.currency .. str or str .. config.currency
end

function DarkRP.formatMoney(n)
    if not n then return attachCurrency("0") end

    if n >= 1e14 then return attachCurrency(tostring(n)) end
    if n <= -1e14 then return "-" .. attachCurrency(tostring(math.abs(n))) end

    local config = GAMEMODE.Config

    local negative = n < 0

    n = tostring(math.abs(n))

    local dp = string.find(n, ".", 1, true) or #n + 1

    for i = dp - 4, 1, -3 do
        n = n:sub(1, i) .. config.currencyThousandSeparator .. n:sub(i + 1)
    end

    -- Make sure the amount is padded with zeroes
    if n[#n - 1] == "." then
        n = n .. "0"
    end

    return (negative and "-" or "") .. attachCurrency(n)
end

--[[---------------------------------------------------------------------------
Find a player based on given information

Note that there is a searching priority:
  * UserID
  * SteamID64
  * SteamID
  * Nick
  * SteamName

Note also that there are _separate_ loops. This is to make sure the function
gives the same result, regardless of the order in which players are iterated
over.
---------------------------------------------------------------------------]]
function DarkRP.findPlayer(info)
    if not info or info == "" then return nil end
    local pls = player.GetAll()

    local count = #pls
    local numberInfo = tonumber(info)

    -- First check if the input matches a player by UserID or SteamID64. This is
    -- only necessary if the input can be parsed as a number.
    if numberInfo then
        for k = 1, count do
            local v = pls[k]

            if numberInfo == v:UserID() then
                return v
            end
        end

        for k = 1, count do
            local v = pls[k]

            if info == v:SteamID64() then
                return v
            end
        end
    end

    local lowerInfo = string.lower(tostring(info))
    if string.StartsWith(lowerInfo, "steam_") then
        for k = 1, count do
            local v = pls[k]

            if info == v:SteamID() then
                return v
            end
        end
    end

    for k = 1, count do
        local v = pls[k]

        if string.find(string.lower(v:Nick()), lowerInfo, 1, true) ~= nil then
            return v
        end
    end

    for k = 1, count do
        local v = pls[k]

        if string.find(string.lower(v:SteamName()), lowerInfo, 1, true) ~= nil then
            return v
        end
    end

    return nil
end

--[[---------------------------------------------------------------------------
Find multiple players based on a string criterium
Taken from FAdmin]]
---------------------------------------------------------------------------*/
function DarkRP.findPlayers(info)
    if not info then return nil end
    local pls = player.GetAll()
    local found = {}
    local players

    if string.lower(info) == "*" or string.lower(info) == "<all>" then return pls end

    local InfoPlayers = {}
    for A in string.gmatch(info .. ";", "([a-zA-Z0-9:_.]*)[;(,%s)%c]") do
        if A ~= "" then
            table.insert(InfoPlayers, A)
        end
    end

    for _, PlayerInfo in ipairs(InfoPlayers) do
        -- Playerinfo is always to be treated as UserID when it's a number
        -- otherwise people with numbers in their names could get confused with UserID's of other players
        if tonumber(PlayerInfo) then
            local foundPlayer = Player(PlayerInfo)
            if IsValid(foundPlayer) and not found[foundPlayer] then
                found[foundPlayer] = true
                players = players or {}
                table.insert(players, foundPlayer)
            end
            continue
        end

        local stringPlayerInfo = string.lower(PlayerInfo)
        for _, v in ipairs(pls) do
            -- Prevent duplicates
            if found[v] then continue end
            local steamId = v:SteamID()

            -- Find by Steam ID
            if (PlayerInfo == steamId or steamId == "UNKNOWN") or
            -- Find by Partial Nick
            string.find(string.lower(v:Nick()), stringPlayerInfo, 1, true) ~= nil or
            -- Find by steam name
            (v.SteamName and string.find(string.lower(v:SteamName()), stringPlayerInfo, 1, true) ~= nil) then
                found[v] = true
                players = players or {}
                table.insert(players, v)
            end
        end
    end

    return players
end

function meta:getEyeSightHitEntity(searchDistance, hitDistance, filter)
    searchDistance = searchDistance or 100
    hitDistance = (hitDistance or 15) * (hitDistance or 15)

    filter = filter or function(p) return p:IsPlayer() and p ~= self end

    self:LagCompensation(true)

    local shootPos = self:GetShootPos()
    local entities = ents.FindInSphere(shootPos, searchDistance)
    local aimvec = self:GetAimVector()

    local smallestDistance = math.huge
    local foundEnt

    for _, ent in ipairs(entities) do
        if not IsValid(ent) or filter(ent) == false then continue end

        local center = ent:GetPos()

        -- project the center vector on the aim vector
        local projected = shootPos + (center - shootPos):Dot(aimvec) * aimvec

        if aimvec:Dot((projected - shootPos):GetNormalized()) < 0 then continue end

        -- the point on the model that has the smallest distance to your line of sight
        local nearestPoint = ent:NearestPoint(projected)
        local distance = nearestPoint:DistToSqr(projected)

        if distance < smallestDistance then
            local trace = {
                start = self:GetShootPos(),
                endpos = nearestPoint,
                filter = {self, ent}
            }
            local traceLine = util.TraceLine(trace)
            if traceLine.Hit then continue end

            smallestDistance = distance
            foundEnt = ent
        end
    end

    self:LagCompensation(false)

    if smallestDistance < hitDistance then
        return foundEnt, math.sqrt(smallestDistance)
    end

    return nil
end

--[[---------------------------------------------------------------------------
Print the currently available vehicles
---------------------------------------------------------------------------]]
local function GetAvailableVehicles(ply)
    if SERVER and IsValid(ply) and not ply:IsAdmin() then return end
    local print = SERVER and ServerLog or Msg

    print(DarkRP.getPhrase("rp_getvehicles") .. "\n")
    for k in pairs(DarkRP.getAvailableVehicles()) do
        print("\"" .. k .. "\"" .. "\n")
    end
end
if SERVER then
    concommand.Add("rp_getvehicles_sv", GetAvailableVehicles)
else
    concommand.Add("rp_getvehicles", GetAvailableVehicles)
end

--[[---------------------------------------------------------------------------
Whether a player has a DarkRP privilege
---------------------------------------------------------------------------]]
function meta:hasDarkRPPrivilege(priv)
    if FAdmin then
        return FAdmin.Access.PlayerHasPrivilege(self, priv)
    end
    return self:IsAdmin()
end

--[[---------------------------------------------------------------------------
Convenience function to return the players sorted by name
---------------------------------------------------------------------------]]
function DarkRP.nickSortedPlayers()
    local plys = player.GetAll()
    table.sort(plys, function(a,b) return a:Nick() < b:Nick() end)
    return plys
end

--[[---------------------------------------------------------------------------
Convert a string to a table of arguments
---------------------------------------------------------------------------]]
local bitlshift, stringgmatch, stringsub, tableinsert = bit.lshift, string.gmatch, string.sub, table.insert
function DarkRP.explodeArg(arg)
    local args = {}

    local from, to, diff = 1, 0, 0
    local inQuotes, wasQuotes = false, false

    for c in stringgmatch(arg, '.') do
        to = to + 1

        if c == '"' then
            inQuotes = not inQuotes
            wasQuotes = true

            continue
        end

        if c == ' ' and not inQuotes then
            diff = wasQuotes and 1 or 0
            wasQuotes = false
            tableinsert(args, stringsub(arg, from + diff, to - 1 - diff))
            from = to + 1
        end
    end
    diff = wasQuotes and 1 or 0

    if from ~= to + 1 then tableinsert(args, stringsub(arg, from + diff, to + 1 - bitlshift(diff, 1))) end

    return args
end

--[[---------------------------------------------------------------------------
Initialize Physics, throw an error on failure
---------------------------------------------------------------------------]]
function DarkRP.ValidatedPhysicsInit(ent, solidType, hint)
    solidType = solidType or SOLID_VPHYSICS

    if ent:PhysicsInit(solidType) then return true end

    local class = ent:GetClass()

    if solidType == SOLID_BSP then
        DarkRP.errorNoHalt(string.format("%s has no physics and will be motionless", class), 2, {
            "Is this a brush model? SOLID_BSP physics cannot initialize on entities that don't have brush models",
            "The physics limit may have been hit",
            hint
        })

        return false
    end

    if solidType == SOLID_VPHYSICS then
        local mdl = ent:GetModel()

        if not mdl or mdl == "" then
            DarkRP.errorNoHalt(string.format("Cannot init physics on entity \"%s\" because it has no model", class), 2, {hint})
            return false
        end

        mdl = string.lower(mdl)

        if util.IsValidProp(mdl) then
            -- Has physics, we must have hit the limit
            DarkRP.errorNoHalt(string.format("physics limit hit - %s will be motionless", class), 2, {hint})

            return false
        end

        if not file.Exists(mdl, "GAME") then
            DarkRP.errorNoHalt(string.format("%s has missing model \"%s\" and will be invisible and motionless", class, mdl), 2, {
                "Is the model path correct?",
                "Is the model from an addon that is not installed?",
                "Is the model from a game that isn't (properly) mounted? E.g. Counter Strike: Source",
                hint
            })

            return false
        end

        DarkRP.errorNoHalt(string.format("%s has model \"%s\" with no physics and will be motionless", class, mdl), 2, {
            "Does this model have an associated physics model (modelname.phy)?",
            "Is this model supposed to have physics? Many models, like effects and view models aren't made to have physics",
            hint
        })

        return false
    end

    DarkRP.errorNoHalt(string.format("Unable to initilize physics on entity \"%s\"", class, {hint}), 2)

    return false
end

--[[---------------------------------------------------------------------------
Like tonumber, but makes sure it's an integer
---------------------------------------------------------------------------]]
function DarkRP.toInt(value)
    value = tonumber(value)
    return value and math.floor(value)
end

--[[-------------------------------------------------------------------------
Check the database for integrity errors. Use in cases when stuff doesn't load
on restart, or you get corruption errors.
---------------------------------------------------------------------------]]
if SERVER then util.AddNetworkString("DarkRP_databaseCheckMessage") end
if CLIENT then net.Receive("DarkRP_databaseCheckMessage", fc{print, net.ReadString}) end

local function checkDatabase(ply)
    local dbFile = SERVER and "sv.db" or "cl.db"
    local display = (CLIENT or not IsValid(ply)) and print or function(msg)
            net.Start("DarkRP_databaseCheckMessage")
            net.WriteString(msg)
            net.Send(ply)
        end

    if SERVER and IsValid(ply) and not ply:IsSuperAdmin() then
        display("You must be superadmin")
        return
    end

    if MySQLite and MySQLite.isMySQL() then
        display(string.format([[WARNING: DarkRP is using MySQL. This only
    checks the local SQLite database stored in the %s file in the
    garrysmod/ folder. The check will continue.]], dbFile))
    end

    local check = sql.QueryValue("PRAGMA INTEGRITY_CHECK")
    if check == false then
        display([[The query to check the database failed. Shit's surely
    fucked, but the cause is unknown.]])
        return
    end

    if check == "ok" then
        display(string.format("Your %s database file is good.", dbFile))
        return
    end

    display(string.format([[There are errors in your %s database file. It's corrupt!

    This can cause the following problems:
    - Data not loading, think of blocked models, doors, players' money and RP names
    - Settings resetting to their default values
    - Lua errors on startup

    The cause of the problem is that the %s file in your garrysmod/ folder on
    %s is corrupt. How this came to be is unknown, but here's what you can do to solve it:

    - Delete %s, and run a file integrity check. Warning: You will lose ALL data stored in it!
    - Take the file and try to repair it. This is sadly something that requires some technical knowledge,
      and may not always succeed.

    The specific error, by the way, is as follows:
    %s
    ]], dbFile, dbFile, SERVER and "the server" or "your own computer", dbFile, check))

end
concommand.Add("darkrp_check_db_" .. (SERVER and "sv" or "cl"), checkDatabase)
