---@class PointProperties
---@field coords vector3
---@field distance number
---@field onEnter? fun(self: CPoint)
---@field onExit? fun(self: CPoint)
---@field nearby? fun(self: CPoint)
---@field [string] any

---@class CPoint : PointProperties
---@field id number
---@field currentDistance number
---@field isClosest? boolean
---@field remove fun()

---@type table<number, CPoint>
local points = {}
---@type CPoint[]
local nearbyPoints = {}
local nearbyCount = 0
---@type CPoint?
local closestPoint
local tick

local function removePoint(self)
    if closestPoint?.id == self.id then
        closestPoint = nil
    end

    lib.grid.removeEntry(self)

    points[self.id] = nil
end

CreateThread(function()
    while true do
        if nearbyCount ~= 0 then
            table.wipe(nearbyPoints)
            nearbyCount = 0
        end

        local coords = GetEntityCoords(cache.ped)
        local newPoints = lib.grid.getNearbyEntries(coords, function(entry) return entry.remove == removePoint end) --[[@as CPoint[] ]]
        local cellX, cellY = lib.grid.getCellPosition(coords)
        cache.coords = coords
        closestPoint = nil

        if cellX ~= cache.lastCellX or cellY ~= cache.lastCellY then
            for i = 1, #nearbyPoints do
                local point = nearbyPoints[i]

                if point.inside then
                    local distance = #(coords - point.coords)

                    if distance > point.radius then
                        if point.onExit then point:onExit() end

                        point.inside = nil
                        point.currentDistance = nil
                    end
                end
            end

            cache.lastCellX = cellX
            cache.lastCellY = cellY
        end

        for i = 1, #newPoints do
            local point = newPoints[i]
            local distance = #(coords - point.coords)

            if distance <= point.radius then
                point.currentDistance = distance

                if not closestPoint or distance < (closestPoint.currentDistance or point.radius) then
                    if closestPoint then closestPoint.isClosest = nil end

                    point.isClosest = true
                    closestPoint = point
                end

                if point.nearby then
                    nearbyCount += 1
					nearbyPoints[nearbyCount] = point
				end

                if point.onEnter and not point.inside then
                    point.inside = true
                    point:onEnter()
                end
            elseif point.currentDistance then
                if point.onExit then point:onExit() end

                point.inside = nil
                point.currentDistance = nil
            end
        end

        if not tick then
            if nearbyCount ~= 0 then
                tick = SetInterval(function()
                    for i = nearbyCount, 1, -1 do
                        local point = nearbyPoints[i]

                        if point and point.nearby then
                            point:nearby()
                        end
                    end
                end)
            end
        elseif nearbyCount == 0 then
            tick = ClearInterval(tick)
        end

        Wait(300)
    end
end)

local function toVector(coords)
    local _type = type(coords)

    if _type ~= 'vector3' then
        if _type == 'table' or _type == 'vector4' then
            return vec3(coords[1] or coords.x, coords[2] or coords.y, coords[3] or coords.z)
        end

        error(("expected type 'vector3' or 'table' (received %s)"):format(_type))
    end

    return coords
end

lib.points = {}

---@return CPoint
---@overload fun(data: PointProperties): CPoint
---@overload fun(coords: vector3, distance: number, data?: PointProperties): CPoint
function lib.points.new(...)
    local args = { ... }
    local id = #points + 1
    local self

    -- Support sending a single argument containing point data
    if type(args[1]) == 'table' then
        self = args[1]
        self.id = id
        self.remove = removePoint
    else
        -- Backwards compatibility for original implementation (args: coords, distance, data)
        self = {
            id = id,
            coords = args[1],
            remove = removePoint,
        }
    end

    self.coords = toVector(self.coords)
    self.distance = self.distance or args[2]
    self.radius = self.distance

    if args[3] then
        for k, v in pairs(args[3]) do
            self[k] = v
        end
    end

    lib.grid.addEntry(self)
    points[id] = self

    return self
end

function lib.points.getAllPoints() return points end

function lib.points.getNearbyPoints() return nearbyPoints end

---@return CPoint?
function lib.points.getClosestPoint() return closestPoint end

---@deprecated
lib.points.closest = lib.points.getClosestPoint

return lib.points
