-- luacheck no new globals
utils = {}

--- @alias XyPair {
---   x: number,
---   y: number,
--- }

--- @alias Point3d {
---   x: number,
---   y: number,
---   z: number,
--- }

local sqrt <const> = math.sqrt

function utils.easingHill(t, b, c, d)
    c = c + 0.0 -- convert to float to prevent integer overflow
    t = t / d
    t = ((t * 2) - 1)
    t = t * t
    return (c * t) + b
end

--- @alias StaticAnimator {
---   currentValue: fun(self): number;
---   reset: fun(self, durationMs: number | nil);
---   ended: fun(self): boolean;
--- }

--- Build an "animator" whose `:currentValue()` always returns the given value.
--- Essentially an "empty object" pattern for initial object positions.
---@param value number
---@return StaticAnimator
function utils.staticAnimator(value)
    return {
        currentValue = function(_)
            return value
        end,
        reset = function(_) end,
        ended = function(_)
            return true
        end,
    }
end

---@param x number
---@param y number
---@return XyPair
function utils.xy(x, y)
    return {
        x = x,
        y = y,
    }
end

--- Returns the normalized vector as two values, plus the distance between the given points.
---@param x1 number
---@param y1 number
---@param x2 number
---@param y2 number
---@return number x, number y, number distance
function utils.normalizeVector(x1, y1, x2, y2)
    local distance, x, y = utils.distanceBetween(x1, y1, x2, y2)
    return x / distance, y / distance, distance
end

--- Push the given obect at the given speed toward a target. Speed should be pre-multiplied by the frame's delta time.
--- Stops when within 1. Returns true only if the object did actually move.
---@param mover { x: number, y: number }
---@param speed number
---@param target { x: number, y: number }
---@param tau number | nil
---@return boolean isStillMoving
function utils.moveAtSpeed(mover, speed, target, tau)
    local x, y, distance = utils.normalizeVector(mover.x, mover.y, target.x, target.y)

    if distance == 0 then
        return false
    end

    if distance > (tau or 1) then
        mover.x = mover.x - (x * speed)
        mover.y = mover.y - (y * speed)
    else
        mover.x = target.x
        mover.y = target.y
    end

    return true
end

---@param acceptableGap number
---@param n1 number
---@param n2 number
---@return boolean n1 is within acceptableGap of n2
function utils.within(acceptableGap, n1, n2)
    return math.abs(n1 - n2) < acceptableGap
end

---@generic T
---@param array T[]
---@param condition fun(T): boolean
---@return T[]
function utils.filter(array, condition)
    local newArray = {}
    for _, element in pairs(array) do
        if condition(element) then
            newArray[#newArray + 1] = element
        end
    end
    return newArray
end

---@generic T
---@param array T[]
---@param condition fun(T): boolean
---@return T | nil
function utils.first(array, condition)
    for _, element in ipairs(array) do
        if condition(element) then
            return element
        end
    end
    return nil
end

---@param x1 number
---@param y1 number
---@param x2 number
---@param y2 number
---@return number distance, number x, number y
function utils.distanceBetween(x1, y1, x2, y2)
    local x = x1 - x2
    local y = y1 - y2
    return sqrt((x * x) + (y * y)), x, y
end

---@param point1 XyPair
---@param point2 XyPair
---@return number distance, number x, number y
function utils.distanceBetweenPoints(point1, point2)
    local x = point1.x - point2.x
    local y = point1.y - point2.y
    return sqrt((x * x) + (y * y)), x, y
end

---@param x1 number
---@param y1 number
---@param z1 number
---@param x2 number
---@param y2 number
---@param z2 number
---@return number distance, number x, number y, number z
function utils.distanceBetweenZ(x1, y1, z1, x2, y2, z2)
    local x = x1 - x2
    local y = y1 - y2
    local z = z1 - z2
    return sqrt((x * x) + (y * y) + (z * z)), x, y, z
end

--- Returns the base being touched by the player at (x,y), or nil, if no base is being touched
---@param x number
---@param y number
---@return Base | nil
function utils.isTouchingBase(x, y)
    return utils.first(C.Bases, function(base)
        return utils.distanceBetween(x, y, base.x, base.y) < C.BaseHitbox
    end)
end

---@param base Base
---@return Runner | nil runner The runner whose next base matches the given base.
function utils.getRunnerWithNextBase(runners, base)
    return utils.first(runners, function(runner)
        return runner.nextBase == base
    end)
end

--- Returns true only if the point is below the given line, within the x bounds of said line, and above the bottomBound.
---@param point XyPair
---@param line1 XyPair
---@param line2 XyPair
---@param bottomBound number
---@return boolean
function utils.pointDirectlyUnderLine(point, line1, line2, bottomBound)
    -- This check currently assumes right-handedness.
    -- I.e. it assumes the ball is to the right of batBaseX
    if point.x < line1.x or point.x > line2.x or point.y > bottomBound then
        return false
    end

    return utils.pointUnderLine(point.x, point.y, line1.x, line1.y, line2.x, line2.y)
end

--- Returns true if the given point is anywhere above the given line, with no upper bound.
--- This, if used for home run calculations, would not take into account balls that curve around the foul poles.
---@param point XyPair
---@param linePoints XyPair[]
---@return boolean
function utils.pointIsSquarelyAboveLine(point, linePoints)
    for i = 2, #linePoints do
        local prev = linePoints[i - 1]
        local next = linePoints[i]
        if point.x >= prev.x and point.x <= next.x then
            return not utils.pointUnderLine(point.x, point.y, prev.x, prev.y, next.x, next.y)
        end
    end
    return false
end

--- Returns true only if the point is below the given line.
---@param pointX number
---@param pointY number
---@param lineX1 number
---@param lineY1 number
---@param lineX2 number
---@param lineY2 number
---@return boolean
function utils.pointUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2)
    local m = (lineY2 - lineY1) / (lineX2 - lineX1)

    -- y = mx + b
    -- b = y1 - (m * x1)
    local b = lineY1 - (m * lineX1)
    local yOnLine = (m * pointX) + b
    local yP = pointY
    local yDelta = yOnLine - yP

    return yDelta <= 0
end

--- Returns true if a ball landing at destX,destY will be foul.
---@param destX number
---@param destY number
function utils.isFoulBall(destX, destY)
    local leftLine = C.LeftFoulLine
    local rightLine = C.RightFoulLine

    return utils.pointUnderLine(destX, destY, leftLine.x1, leftLine.y1, leftLine.x2, leftLine.y2)
        or utils.pointUnderLine(destX, destY, rightLine.x1, rightLine.y1, rightLine.x2, rightLine.y2)
end

--- Returns the nearest position object from the given point, as well as its distance from that point
---@generic T : { x: number, y: number | nil }
---@param array T[]
---@param x number
---@param y number
---@param extraCondition fun(t: T): boolean
---@return T nearest,number |nil distance
function utils.getNearestOf(array, x, y, extraCondition)
    local nearest, nearestDistance = nil, nil
    for _, element in pairs(array) do
        if not extraCondition or extraCondition(element) then
            if nearest == nil then
                nearest = element
                nearestDistance = utils.distanceBetween(element.x, element.y, x, y)
            else
                local distance = utils.distanceBetween(element.x, element.y, x, y)
                if distance < nearestDistance then
                    nearest = element
                    nearestDistance = distance
                end
            end
        end
    end

    return nearest, nearestDistance
end

---@param stats Statistics
---@return number homeScore, number awayScore
function utils.totalScores(stats)
    local homeScore = 0
    local awayScore = 0
    for _, inning in pairs(stats.innings) do
        homeScore = homeScore + inning.home.score
        awayScore = awayScore + inning.away.score
    end

    return homeScore, awayScore
end

if not playdate then
    return utils
end
