local MarginY <const> = 70

local SmallFont <const> = FontFullCircle
local ScoreFont <const> = AshevilleSans14Bold
local NumWidth <const> = ScoreFont:getTextWidth("0")
local NumHeight <const> = ScoreFont:getHeight()
local AwayWidth <const> = ScoreFont:getTextWidth("AWAY")
local InningMargin = 4

local InningDrawWidth <const> = (InningMargin * 2) + (NumWidth * 2)
local ScoreDrawHeight = NumHeight * 2

-- luacheck: ignore 143
---@type pd_graphics_lib
local gfx = playdate.graphics

local function formatScore(n)
    if n <= 9 then
        return "  " .. n
    elseif n <= 19 then
        return " " .. n
    else
        return tostring(n)
    end
end

local HomeY <const> = -4 + (NumHeight * 2) + MarginY
local AwayY <const> = -4 + (NumHeight * 3) + MarginY

local function drawInning(x, inningNumber, homeScore, awayScore)
    gfx.setColor(gfx.kColorBlack)
    gfx.setColor(gfx.kColorWhite)
    gfx.setLineWidth(1)
    gfx.drawRect(x, 34 + MarginY, InningDrawWidth, ScoreDrawHeight)

    inningNumber = " " .. inningNumber
    homeScore = formatScore(homeScore)
    awayScore = formatScore(awayScore)

    x = x - 8 + (InningDrawWidth / 2)
    ScoreFont:drawTextAligned(inningNumber, x, -4 + NumHeight + MarginY, gfx.kAlignRight)
    ScoreFont:drawTextAligned(awayScore, x, HomeY, gfx.kAlignRight)
    ScoreFont:drawTextAligned(homeScore, x, AwayY, gfx.kAlignRight)
end

---@class BoxScore
---@field stats Statistics
---@field private targetY number
BoxScore = {}

---@param stats Statistics
---@return BoxScore
function BoxScore.new(stats)
    return setmetatable({
        stats = stats,
        targetY = 0,
    }, { __index = BoxScore })
end

function BoxScore:drawBoxScore()
    local inningStart = 4 + (AwayWidth * 1.5)
    local widthAndMarg = InningDrawWidth + 4
    ScoreFont:drawTextAligned(" HOME", 10, HomeY, gfx.kAlignRight)
    ScoreFont:drawTextAligned("AWAY", 10, AwayY, gfx.kAlignRight)
    for i = 1, #self.stats.innings do
        local inningStats = self.stats.innings[i]
        drawInning(inningStart + ((i - 1) * widthAndMarg), i, inningStats.home.score, inningStats.away.score)
    end
    local homeScore, awayScore = utils.totalScores(self.stats)
    drawInning(4 + inningStart + (widthAndMarg * #self.stats.innings), "F", homeScore, awayScore)
    ScoreFont:drawTextAligned("v", C.Center.x, C.Screen.H - 40, gfx.kAlignCenter)
end

local GraphM = 10
local GraphW = C.Screen.W - (GraphM * 2)
local GraphH = C.Screen.H - (GraphM * 2)

function BoxScore:drawScoreGraph(y)
    -- TODO: Actually draw score legend

    -- Offset by 2 to support a) the zero-index b) the score legend
    local segmentWidth = GraphW / (#self.stats.innings + 2)

    local legendX = segmentWidth * (#self.stats.innings + 2) - GraphM
    gfx.drawLine(GraphM / 2, y + GraphM + GraphH, legendX, y + GraphM + GraphH)
    gfx.drawLine(legendX, y + GraphM, legendX, y + GraphH + GraphM)

    gfx.setLineWidth(3)
    local homeScore, awayScore = utils.totalScores(self.stats)
    local highestScore = math.max(homeScore, awayScore)

    local heightPerPoint = (GraphH - 6) / highestScore

    function point(inning, score)
        return utils.xy(GraphM + (inning * segmentWidth), y + GraphM + GraphH + (score * -heightPerPoint))
    end

    function drawLine(teamId)
        local linePoints = { point(0, 0) }
        local scoreTotal = 0
        for i, inning in ipairs(self.stats.innings) do
            scoreTotal = scoreTotal + inning[teamId].score
            linePoints[#linePoints + 1] = point(i, scoreTotal)
        end
        dbg.drawLine(linePoints)
        local finalPoint = linePoints[#linePoints]
        SmallFont:drawTextAligned(string.upper(teamId), finalPoint.x + 3, finalPoint.y - 7, gfx.kAlignRight)
    end

    drawLine("home")
    gfx.setDitherPattern(0.5)
    drawLine("away")
    gfx.setDitherPattern(0)
end

---@param realHit XyPair
---@return XyPair
function convertHitToMini(realHit)
    -- Convert to all-positive y
    local y = realHit.y + C.Screen.H
    y = y / 2

    local x = realHit.x + C.Screen.W
    x = x / 3
    return utils.xy(x, y)
end

function BoxScore:drawHitChart(y)
    local leftMargin = 8
    GrassBackgroundSmall:drawCentered(C.Center.x, y + C.Center.y + 54)
    gfx.setLineWidth(1)
    ScoreFont:drawTextAligned("AWAY", leftMargin, y + C.Screen.H - NumHeight, gfx.kAlignRight)
    gfx.setColor(gfx.kColorBlack)
    gfx.setDitherPattern(0.5, gfx.image.kDitherTypeBayer2x2)
    gfx.fillRect(leftMargin, y + C.Screen.H - NumHeight, ScoreFont:getTextWidth("AWAY"), NumHeight)

    gfx.setColor(gfx.kColorWhite)
    gfx.setDitherPattern(0.5)
    for _, inning in ipairs(self.stats.innings) do
        for _, hit in ipairs(inning.away.hits) do
            local miniHitPos = convertHitToMini(hit)
            gfx.fillCircleAtPoint(miniHitPos.x + 10, miniHitPos.y + y, 4)
        end
    end
    gfx.setColor(gfx.kColorWhite)
    gfx.setDitherPattern(0)
    ScoreFont:drawTextAligned(" HOME", leftMargin, y + C.Screen.H - (NumHeight * 2), gfx.kAlignRight)
    for _, inning in ipairs(self.stats.innings) do
        for _, hit in ipairs(inning.home.hits) do
            local miniHitPos = convertHitToMini(hit)
            gfx.fillCircleAtPoint(miniHitPos.x + 10, miniHitPos.y + y, 4)
        end
    end
end

local screens = {
    BoxScore.drawBoxScore,
    BoxScore.drawScoreGraph,
    BoxScore.drawHitChart,
}

function BoxScore:render()
    local originalDrawMode = gfx.getImageDrawMode()
    gfx.clear(gfx.kColorBlack)
    gfx.setImageDrawMode(gfx.kDrawModeInverted)
    gfx.setColor(gfx.kColorBlack)

    for i, screen in ipairs(screens) do
        screen(self, (i - 1) * C.Screen.H)
    end

    gfx.setImageDrawMode(originalDrawMode)
end

local renderedImage

function BoxScore:update()
    if not renderedImage then
        renderedImage = gfx.image.new(C.Screen.W, C.Screen.H * #screens)
        gfx.pushContext(renderedImage)
        self:render()
        gfx.popContext()
    end

    local deltaSeconds = playdate.getElapsedTime()
    playdate.resetElapsedTime()

    gfx.setDrawOffset(0, self.targetY)
    renderedImage:draw(0, 0)

    local crankChange = playdate.getCrankChange()
    if crankChange ~= 0 then
        self.targetY = self.targetY - (crankChange * 0.8)
    else
        local closestScreen = math.floor(0.5 + (self.targetY / C.Screen.H)) * C.Screen.H
        if math.abs(self.targetY - closestScreen) > 3 then
            local needsIncrease = self.targetY < closestScreen
            local change = needsIncrease and 200 * deltaSeconds or -200 * deltaSeconds
            self.targetY = self.targetY + change
        end
    end
    self.targetY = math.max(math.min(self.targetY, 0), -C.Screen.H * (#screens - 1))
end
