---@alias SimpleAnimator { currentValue: fun(self): number; reset: fun(self, durationMs: number | nil) }
---@alias Pitch fun(ball: Ball): { x: SimpleAnimator, y: SimpleAnimator, z: SimpleAnimator | nil }

---@type pd_graphics_lib
local gfx <const> = playdate.graphics

local StrikeZoneWidth <const> = C.StrikeZoneEndX - C.StrikeZoneStartX

-- TODO? Also degrade speed
---@param accuracy number
---@return number xValueToMissBy
function getPitchMissBy(accuracy)
    accuracy = accuracy or 1.0
    local missBy = (1 - accuracy) * StrikeZoneWidth * 3
    if math.random() > 0.5 then
        missBy = missBy * -1
    end
    return missBy
end

---@type Pitch[]
Pitches = {
    -- Fastball
    function(accuracy)
        return {
            x = gfx.animator.new(
                0,
                C.PitchStart.x,
                getPitchMissBy(accuracy) + C.PitchStart.x,
                playdate.easingFunctions.linear
            ),
            y = gfx.animator.new(C.PitchFlyMs / 1.3, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear),
        }
    end,
    -- Curve ball
    function(accuracy)
        return {
            x = gfx.animator.new(
                C.PitchFlyMs,
                getPitchMissBy(accuracy) + C.PitchStart.x + 20,
                C.PitchStart.x,
                utils.easingHill
            ),
            y = gfx.animator.new(C.PitchFlyMs, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear),
        }
    end,
    -- Slider
    function(accuracy)
        return {
            x = gfx.animator.new(
                C.PitchFlyMs,
                getPitchMissBy(accuracy) + C.PitchStart.x - 20,
                C.PitchStart.x,
                utils.easingHill
            ),
            y = gfx.animator.new(C.PitchFlyMs, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear),
        }
    end,
    -- Wobbleball
    function(accuracy, ball)
        local missBy = getPitchMissBy(accuracy)
        return {
            x = {
                currentValue = function()
                    return missBy
                        + C.PitchStart.x
                        + (10 * math.sin((ball.yAnimator:currentValue() - C.PitchStart.y) / 10))
                end,
                reset = function() end,
            },
            y = gfx.animator.new(C.PitchFlyMs * 1.3, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear),
        }
    end,
}

---@alias PitchOutcome "StrikeOut" | "Walk"

---@type table<string, PitchOutcome>
PitchOutcomes = {
    StrikeOut = "StrikeOut",
    Walk = "Walk",
}

pitchTracker = {
    --- Position of the pitch, or nil, if one has not been recorded.
    ---@type number | nil
    recordedPitchX = nil,

    -- TODO: Replace with timer, repeatedly reset, instead of constantly setting to 0
    secondsSinceLastPitch = 0,

    strikes = 0,
    balls = 0,
}

function pitchTracker:reset()
    self.strikes = 0
    self.balls = 0
end

---@param ball XyPair
function pitchTracker:recordIfPassed(ball)
    if ball.y < C.StrikeZoneStartY then
        self.recordedPitchX = nil
    elseif not self.recordedPitchX then
        self.recordedPitchX = ball.x
    end
end

---@param didSwing boolean
---@param fieldingTeamInningData TeamInningData
---@return PitchOutcome | nil
function pitchTracker:updatePitchCounts(didSwing, fieldingTeamInningData)
    if not self.recordedPitchX then
        return
    end

    local currentPitchingStats = fieldingTeamInningData.pitching

    if didSwing or self.recordedPitchX > C.StrikeZoneStartX and self.recordedPitchX < C.StrikeZoneEndX then
        self.strikes = self.strikes + 1
        currentPitchingStats.strikes = currentPitchingStats.strikes + 1
        if self.strikes >= 3 then
            self:reset()
            return PitchOutcomes.StrikeOut
        end
    else
        self.balls = self.balls + 1
        currentPitchingStats.balls = currentPitchingStats.balls + 1
        if self.balls >= 4 then
            self:reset()
            return PitchOutcomes.Walk
        end
    end
end

-----------------
-- Throw Meter --
-----------------

throwMeter = {
    MinCharge = 25,

    idealPower = 50,

    --- Used at draw-time only.
    ---@type number
    lastReadThrow = nil,

    --- Used at draw-time only.
    ---@type Fielder | nil
    lastThrower = nil,

    --- Used at draw-time only.
    ---@type boolean
    wasPerfect = false,
}

local MaxPowerRatio <const> = 1.5

--- Returns nil when a throw is NOT requested.
---@param chargeAmount number
---@return number | nil powerRatio, number | nil accuracy, boolean isPerfect
function throwMeter:readThrow(chargeAmount)
    local power = self:readCharge(chargeAmount)
    if not power then
        return nil, nil, false
    end

    local ratio = math.min(power / self.idealPower, MaxPowerRatio)
    self.wasPerfect = math.abs(ratio - 1) < 0.05

    local accuracy = 1
    -- Only throw off accuracy on slow throws
    if ratio >= 1 and not self.wasPerfect then
        accuracy = 1 / ratio
    end

    return ratio * 1.5, accuracy, self.wasPerfect
end

local CrankRecordSec <const> = 0.33

---@alias CrankQueueEntry { time: number, chargeAmount: number }

---@type CrankQueueEntry[]
local crankQueue = {}

--- If (within approx. a third of a second) the crank has moved more than 45 degrees, call that a throw.
---@param chargeAmount number
---@return number | nil
function throwMeter:readCharge(chargeAmount)
    if chargeAmount == 0 then
        return nil
    end

    local currentTimeMs = playdate.getCurrentTimeMilliseconds()
    local minTimeHasPassed = false
    while #crankQueue ~= 0 and (currentTimeMs - crankQueue[1].time) > CrankRecordSec do
        table.remove(crankQueue, 1)
        minTimeHasPassed = true
    end

    crankQueue[#crankQueue + 1] = { time = currentTimeMs, chargeAmount = math.abs(chargeAmount) }

    if not minTimeHasPassed then
        return nil
    end

    local currentCharge = 0
    for _, v in ipairs(crankQueue) do
        currentCharge = currentCharge + v.chargeAmount
    end

    if currentCharge > throwMeter.MinCharge then
        self.lastReadThrow = currentCharge
        crankQueue = {}
        return currentCharge
    else
        return nil
    end
end
