--- @class Fielder {
--- @field x number
--- @field y number
--- @field target XyPair | nil
--- @field speed number

-- luacheck: ignore 631
---@class Fielding
---@field fielders { first: Fielder, second: Fielder, shortstop: Fielder, third: Fielder, pitcher: Fielder, catcher: Fielder, left: Fielder, center: Fielder, right: Fielder }
---@field fielderHoldingBall Fielder | nil
Fielding = {}

FielderDanceAnimator = playdate.graphics.animator.new(1, 10, 0, utils.easingHill)
FielderDanceAnimator.repeatCount = C.DanceBounceCount - 1

---@param name string
---@param speed number
---@return Fielder
local function newFielder(name, speed)
    return {
        name = name,
        speed = speed * C.FielderRunMult,
    }
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", 50),
            center = newFielder("Center", 50),
            right = newFielder("Right", 50),
        },
        ---@type Fielder | nil
        fielderHoldingBall = 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 * -0.6, C.Screen.H * -0.1)
    self.fielders.center.target = utils.xy(C.Center.x, C.Screen.H * -0.4)
    self.fielders.right.target = utils.xy(C.Screen.W * 1.6, self.fielders.left.target.y)
end

---@param deltaSeconds number
---@param fielder Fielder
---@param ball Ball
---@return boolean canCatch
local function updateFielderPosition(deltaSeconds, fielder, ball)
    if fielder.target ~= nil then
        if
            utils.pointIsSquarelyAboveLine(fielder, C.BottomOfOutfieldWall)
            or not utils.moveAtSpeed(fielder, fielder.speed * deltaSeconds, fielder.target)
        then
            fielder.target = nil
        end
    end

    return ball.catchable and utils.distanceBetweenPoints(fielder, ball) < C.BallCatchHitbox
end

-- TODO: Prevent multiple fielders covering the same base.
-- At least in a how-about-everybody-stand-right-here way.

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

    for _, base in ipairs(C.Bases) do
        local nearest = utils.getNearestOf(self.fielders, base.x, base.y, function(fielder)
            -- Skip the pitcher for 2B - they're considered closer than second or shortstop.
            if fielder == self.fielders.pitcher and base == C.Bases[C.Second] then
                return false
            end
            return fielder ~= chasingFielder
        end)
        nearest.target = base
    end
end

--- **Also updates `ball.heldby`**
---@param ball Ball
---@param deltaSeconds number
---@return Fielder | nil fielderHoldingBall nil if no fielder is currently touching the ball
function Fielding:updateFielderPositions(ball, deltaSeconds)
    local fielderHoldingBall
    for _, fielder in pairs(self.fielders) do
        -- TODO: Base this catch on fielder skill?
        local canCatch = updateFielderPosition(deltaSeconds, fielder, ball)
        if canCatch then
            fielderHoldingBall = fielder
            ball.heldBy = fielder -- How much havoc will this wreak?
        end
    end
    -- TODO: The need is growing for a distinction between touching the ball and holding the ball.
    -- Or, at least, fielders need to start *stopping* the ball when they make contact with it.
    -- Right now, a line-drive *through* first will be counted as an out.
    self.fielderHoldingBall = fielderHoldingBall
    return fielderHoldingBall
end

-- TODO? Start moving target fielders close sooner?
---@param field Fielding
---@param targetBase Base
---@param ball { launch: LaunchBall }
---@param throwFlyMs number
local function userThrowToCoroutine(field, targetBase, ball, throwFlyMs)
    while true do
        if field.fielderHoldingBall == nil then
            coroutine.yield()
        else
            local closestFielder = utils.getNearestOf(field.fielders, targetBase.x, targetBase.y, function(fielder)
                return fielder ~= field.fielderHoldingBall -- presumably, this is who will be doing the throwing
            end)

            closestFielder.target = targetBase
            ball:launch(targetBase.x, targetBase.y, playdate.easingFunctions.linear, throwFlyMs)

            return
        end
    end
end

--- Buffer in a fielder throw action.
---@param self table
---@param targetBase Base
---@param ball { launch: LaunchBall }
---@param throwFlyMs number
function Fielding:userThrowTo(targetBase, ball, throwFlyMs)
    local maxTryTimeMs = 5000
    actionQueue:upsert("userThrowTo", maxTryTimeMs, function()
        userThrowToCoroutine(self, targetBase, ball, throwFlyMs)
    end)
end

function Fielding.celebrate()
    FielderDanceAnimator:reset(C.DanceBounceMs)
end

-- luacheck: ignore
if not playdate or playdate.TEST_MODE then
    return { Fielding, newFielder }
end
