---@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

---@type Pitch[]
Pitches = {
    -- Fastball
    function()
        return {
            x = gfx.animator.new(0, C.PitchStart.x, 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()
        return {
            x = gfx.animator.new(C.PitchFlyMs, 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()
        return {
            x = gfx.animator.new(C.PitchFlyMs, C.PitchStart.x - 20, C.PitchStart.x, utils.easingHill),
            y = gfx.animator.new(C.PitchFlyMs, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear),
        }
    end,
    -- Wobbbleball
    function(ball)
        return {
            x = {
                currentValue = function()
                    return 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,
}

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

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
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 = 45,

    value = 0,
    idealPower = 90,
    lastReadThrow = nil,
}

function throwMeter:reset()
    self.value = 0
end

local crankQueue = {}

--- Returns nil when a throw is NOT requested.
---@param chargeAmount number
---@return number | nil powerRatio, boolean isPerfect
function throwMeter:readThrow(chargeAmount)
    local ret = self:applyCharge(chargeAmount)
    if ret then
        local ratio = ret / self.idealPower
        return ratio, math.abs(ratio - 1) < 0.05
    end
    return nil, false
end

--- If (within approx. a third of a second) the crank has moved more than 45 degrees, call that a throw.
---@param chargeAmount number
function throwMeter:applyCharge(chargeAmount)
    if chargeAmount == 0 then
        return
    end
    local currentTimeMs = playdate.getCurrentTimeMilliseconds()
    local removedOne = false
    while #crankQueue ~= 0 and (currentTimeMs - crankQueue[1].time) > 0.33 do
        table.remove(crankQueue, 1)
        removedOne = true -- At least 1/3 second has passed
    end
    crankQueue[#crankQueue + 1] = { time = currentTimeMs, chargeAmount = math.abs(chargeAmount) }

    if not removedOne 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
        print(tostring(currentCharge) .. " from " .. #crankQueue)
        crankQueue = {}
        return currentCharge
    else
        return nil
    end
end
