-- selene: allow(unscoped_variables)
---@class Fielding
---@field fielders table<string, Fielder>
---@field fielderTouchingBall Fielder | nil
Fielding = {}

---@param name string
---@param speed number
---@return Fielder
local function newFielder(name, speed)
    return {
        name = name,
        speed = speed,
    }
end

function Fielding.new()
    return setmetatable({
        fielders = {
            first = newFielder("First", 40),
            second = newFielder("Second", 40),
            shortstop = newFielder("Shortstop", 40),
            third = newFielder("Third", 40),
            pitcher = newFielder("Pitcher", 30),
            catcher = newFielder("Catcher", 35),
            left = newFielder("Left", 40),
            center = newFielder("C.Center", 40),
            right = newFielder("Right", 40),
        },
        ---@type Fielder | nil
        fielderTouchingBall = nil,
    }, { __index = Fielding })
end

--- Actually only benches the infield, because outfielders are far away!
---@param position XyPair
function Fielding:benchTo(position)
    self.fielders.first.target = position
    self.fielders.second.target = position
    self.fielders.shortstop.target = position
    self.fielders.third.target = position
    self.fielders.pitcher.target = position
    self.fielders.catcher.target = position
end

--- Resets the target positions of all fielders to their defaults (at their field positions).
---@param fromOffTheField XyPair | nil If provided, also sets all runners' current position to one centralized location.
function Fielding:resetFielderPositions(fromOffTheField)
    if fromOffTheField then
        for _, fielder in pairs(self.fielders) do
            fielder.x = fromOffTheField.x
            fielder.y = fromOffTheField.y
        end
    end

    self.fielders.first.target = utils.xy(C.Screen.W - 65, C.Screen.H * 0.48)
    self.fielders.second.target = utils.xy(C.Screen.W * 0.70, C.Screen.H * 0.30)
    self.fielders.shortstop.target = utils.xy(C.Screen.W * 0.30, C.Screen.H * 0.30)
    self.fielders.third.target = utils.xy(C.Screen.W * 0.1, C.Screen.H * 0.48)
    self.fielders.pitcher.target = utils.xy(C.PitcherStartPos.x, C.PitcherStartPos.y)
    self.fielders.catcher.target = utils.xy(C.Screen.W * 0.475, C.Screen.H * 0.92)
    self.fielders.left.target = utils.xy(C.Screen.W * -1, C.Screen.H * -0.2)
    self.fielders.center.target = utils.xy(C.Center.x, C.Screen.H * -0.4)
    self.fielders.right.target = utils.xy(C.Screen.W * 2, self.fielders.left.target.y)
end

---@param deltaSeconds number
---@param fielder Fielder
---@param ballPos XyPair
---@return boolean isTouchingBall
local function updateFielderPosition(deltaSeconds, fielder, ballPos)
    if fielder.target ~= nil then
        if not utils.moveAtSpeed(fielder, fielder.speed * deltaSeconds, fielder.target) then
            fielder.target = nil
        end
    end

    return utils.distanceBetweenPoints(fielder, ballPos) < C.BallCatchHitbox
end

--- Selects the nearest fielder to move toward the given coordinates.
--- Other fielders should attempt to cover their bases
---@param self table
---@param ballDestX number
---@param ballDestY number
function Fielding:haveSomeoneChase(ballDestX, ballDestY)
    local chasingFielder = utils.getNearestOf(self.fielders, ballDestX, ballDestY)
    chasingFielder.target = { x = ballDestX, y = ballDestY }

    for _, base in ipairs(C.Bases) do
        local nearest = utils.getNearestOf(self.fielders, base.x, base.y, function(fielder)
            -- For now, skipping the pitcher because they're considered closer to 2B than second or shortstop
            return fielder ~= chasingFielder and fielder ~= self.fielders.pitcher
        end)
        nearest.target = base
    end
end

---@param ball XyPair
---@param deltaSeconds number
---@return Fielder | nil fielderTouchingBall nil if no fielder is currently touching the ball
function Fielding:updateFielderPositions(ball, deltaSeconds)
    local fielderTouchingBall = nil
    for _, fielder in pairs(self.fielders) do
        local isTouchingBall = updateFielderPosition(deltaSeconds, fielder, ball)
        if isTouchingBall then
            fielderTouchingBall = fielder
        end
    end
    self.fielderTouchingBall = fielderTouchingBall
    return fielderTouchingBall
end

-- TODO? Start moving target fielders close sooner?
---@param field table
---@param targetBase Base
---@param launchBall LaunchBall
---@param throwFlyMs number
---@return ActionResult
local function playerThrowToImpl(field, targetBase, launchBall, throwFlyMs)
    if field.fielderTouchingBall == nil then
        return ActionResult.NeedsMoreTime
    end
    local closestFielder = utils.getNearestOf(field.fielders, targetBase.x, targetBase.y, function(fielder)
        return fielder ~= field.fielderTouchingBall -- presumably, this is who will be doing the throwing
    end)

    closestFielder.target = targetBase
    launchBall(targetBase.x, targetBase.y, playdate.easingFunctions.linear, throwFlyMs)
    return ActionResult.Succeeded
end

--- Buffer in a fielder throw action.
---@param self table
---@param targetBase Base
---@param launchBall LaunchBall
---@param throwFlyMs number
function Fielding:playerThrowTo(targetBase, launchBall, throwFlyMs)
    local maxTryTimeMs = 5000
    actionQueue:upsert('playerThrowTo', maxTryTimeMs, function()
        return playerThrowToImpl(self, targetBase, launchBall, throwFlyMs)
    end)
end