-- Rack & Ruin: Darts.lua
-- Classic 501: everyone starts at 501, throws 3 darts per turn, subtracts
-- their total from their remaining score. First to exactly 0 wins.
-- Simplification: no strict "double out" requirement - reaching exactly 0
-- on any dart wins, to keep this approachable. Overshooting below 0 on a
-- turn is a "bust" - that turn's throws don't count and score resets to
-- what it was at the start of the turn.
--
-- Throwing: pick a target (1-20 or Bull), then click Throw to lock in a
-- continuously sweeping accuracy meter. Where the meter lands when you
-- click determines whether you hit a Triple/Double/Single of your target,
-- the Bullseye, or missed entirely.

RackAndRuin = RackAndRuin or {}

local ORANGE = { 1, 0.55, 0.1 }
local GAME_ID = "DARTS"
local STARTING_SCORE = 501
local DARTS_PER_TURN = 3

local frame = CreateFrame("Frame", "RackAndRuinDartsFrame", UIParent, "BackdropTemplate")
frame:SetSize(600, 640)
frame:SetPoint("CENTER")
frame:SetMovable(true)
frame:EnableMouse(true)
frame:RegisterForDrag("LeftButton")
frame:SetScript("OnDragStart", frame.StartMoving)
frame:SetScript("OnDragStop", frame.StopMovingOrSizing)
frame:SetBackdrop({
    bgFile = "Interface\\Buttons\\WHITE8x8",
    edgeFile = "Interface\\Buttons\\WHITE8x8",
    edgeSize = 2,
})
frame:SetBackdropColor(0, 0, 0, 0.97)
frame:SetBackdropBorderColor(unpack(ORANGE))
frame:SetFrameStrata("HIGH")
frame:SetToplevel(true)
frame:Hide()

local statusText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
statusText:SetPoint("TOP", 0, -28)
statusText:SetTextColor(1, 0.85, 0.2)
statusText:SetText("")

local tableText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
tableText:SetPoint("TOPLEFT", 24, -60)
tableText:SetWidth(120)
tableText:SetJustifyH("LEFT")
tableText:SetJustifyV("TOP")
tableText:SetTextColor(0.7, 0.7, 0.7)
tableText:SetText("")

-- ============================================================
-- Dartboard visual
-- ============================================================
local board = frame:CreateTexture(nil, "ARTWORK")
board:SetSize(340, 340)
board:SetPoint("TOP", 0, -56)
board:SetTexture(RackAndRuin.GetDartboardTexture())

local throwsText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
throwsText:SetPoint("TOP", board, "BOTTOM", 0, -10)
throwsText:SetTextColor(0.85, 0.85, 0.85)
throwsText:SetText("")

-- ============================================================
-- Target selector (1-20 + Bull)
-- ============================================================
local targetLabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
targetLabel:SetPoint("TOP", throwsText, "BOTTOM", 0, -12)
targetLabel:SetTextColor(0.8, 0.8, 0.8)
targetLabel:SetText("Aiming at: -")

local function MiniButton(parent, text)
    local btn = CreateFrame("Button", nil, parent, "BackdropTemplate")
    btn:SetSize(34, 26)
    btn:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    btn:SetBackdropColor(0, 0, 0, 1)
    btn:SetBackdropBorderColor(unpack(ORANGE))

    local label = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    label:SetPoint("CENTER")
    label:SetText(text)
    label:SetTextColor(1, 0.85, 0.6)
    btn.label = label

    return btn
end

local selectedTarget = nil -- number 1-20, or "BULL"
local targetButtons = {}

local function ClearTargetVisuals()
    for _, btn in pairs(targetButtons) do
        btn:SetBackdropColor(0, 0, 0, 1)
    end
end

local targetRow = CreateFrame("Frame", nil, frame)
targetRow:SetSize(560, 60)
targetRow:SetPoint("TOP", targetLabel, "BOTTOM", 0, -8)

for i = 1, 20 do
    local btn = MiniButton(targetRow, tostring(i))
    local col = (i - 1) % 10
    local row = math.floor((i - 1) / 10)
    btn:SetPoint("TOPLEFT", col * 36, -row * 28)
    btn:SetScript("OnClick", function()
        ClearTargetVisuals()
        selectedTarget = i
        targetLabel:SetText("Aiming at: " .. i)
        btn:SetBackdropColor(0.35, 0.15, 0, 1)
    end)
    targetButtons[tostring(i)] = btn
end

local bullBtn = MiniButton(targetRow, "Bull")
bullBtn:SetSize(50, 26)
bullBtn:SetPoint("TOPLEFT", 10 * 36 + 6, -28)
bullBtn:SetScript("OnClick", function()
    ClearTargetVisuals()
    selectedTarget = "BULL"
    targetLabel:SetText("Aiming at: Bullseye")
    bullBtn:SetBackdropColor(0.35, 0.15, 0, 1)
end)
targetButtons["BULL"] = bullBtn

-- ============================================================
-- Timing/accuracy meter
-- ============================================================
local meterFrame = CreateFrame("Frame", nil, frame, "BackdropTemplate")
meterFrame:SetSize(460, 28)
meterFrame:SetPoint("TOP", targetRow, "BOTTOM", 0, -16)
meterFrame:SetBackdrop({
    bgFile = "Interface\\Buttons\\WHITE8x8",
    edgeFile = "Interface\\Buttons\\WHITE8x8",
    edgeSize = 2,
})
meterFrame:SetBackdropColor(0, 0, 0, 1)
meterFrame:SetBackdropBorderColor(unpack(ORANGE))

-- Colored zone segments matching ResolveThrow's bands below (for a
-- non-Bull target - Bull targets reinterpret everything outside the
-- 0.90-1.0 range as a miss, but the visual stays the same either way).
-- Zones left-to-right: Miss | Single | Double | Single | Triple | Bull
local ZONES = {
    { from = 0.00, to = 0.10, color = { 0.3, 0.3, 0.3, 1 } },   -- miss
    { from = 0.10, to = 0.25, color = { 0.85, 0.85, 0.85, 1 } }, -- single
    { from = 0.25, to = 0.45, color = { 0.2, 0.7, 0.3, 1 } },    -- double
    { from = 0.45, to = 0.70, color = { 0.85, 0.85, 0.85, 1 } }, -- single
    { from = 0.70, to = 0.90, color = { 0.8, 0.15, 0.15, 1 } },  -- triple
    { from = 0.90, to = 0.97, color = { 0.85, 0.65, 0.1, 1 } },  -- outer bull
    { from = 0.97, to = 1.00, color = { 1, 0.85, 0.2, 1 } },     -- inner bull
}

local meterWidth = meterFrame:GetWidth() - 4
for _, zone in ipairs(ZONES) do
    local seg = meterFrame:CreateTexture(nil, "ARTWORK")
    seg:SetColorTexture(unpack(zone.color))
    seg:SetPoint("LEFT", meterFrame, "LEFT", 2 + zone.from * meterWidth, 0)
    seg:SetSize((zone.to - zone.from) * meterWidth, 24)
end

-- Labels below the bar. The two bull segments are narrow, so they share
-- one centered "BULL" label spanning both instead of two illegible slivers.
local LABELS = {
    { from = 0.00, to = 0.10, text = "MISS" },
    { from = 0.10, to = 0.25, text = "SINGLE" },
    { from = 0.25, to = 0.45, text = "DOUBLE" },
    { from = 0.45, to = 0.70, text = "SINGLE" },
    { from = 0.70, to = 0.90, text = "TRIPLE" },
    { from = 0.90, to = 1.00, text = "BULL" },
}
for _, zone in ipairs(LABELS) do
    local lbl = meterFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    local mid = (zone.from + zone.to) / 2
    lbl:SetPoint("TOP", meterFrame, "BOTTOMLEFT", 2 + mid * meterWidth, -4)
    lbl:SetText(zone.text)
    lbl:SetTextColor(0.85, 0.85, 0.85)
end

local meterIndicator = meterFrame:CreateTexture(nil, "OVERLAY")
meterIndicator:SetColorTexture(1, 1, 1, 1)
meterIndicator:SetSize(4, 28)
meterIndicator:SetPoint("CENTER", meterFrame, "LEFT", 0, 0)

local meterRunning = false
local meterPos = 0 -- 0 to 1, ping-pongs
local meterDir = 1
local METER_SPEED = 0.9 -- fraction of bar per second

meterFrame:SetScript("OnUpdate", function(self, elapsed)
    if not meterRunning then return end
    meterPos = meterPos + meterDir * METER_SPEED * elapsed
    if meterPos >= 1 then
        meterPos = 1
        meterDir = -1
    elseif meterPos <= 0 then
        meterPos = 0
        meterDir = 1
    end
    local w = meterFrame:GetWidth() - 4
    meterIndicator:SetPoint("CENTER", meterFrame, "LEFT", 2 + meterPos * w, 0)
end)

-- ============================================================
-- Buttons
-- ============================================================
local function StyledButton(parent, text, width, height)
    local btn = CreateFrame("Button", nil, parent, "BackdropTemplate")
    btn:SetSize(width or 170, height or 42)
    btn:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 2,
    })
    btn:SetBackdropColor(0, 0, 0, 1)
    btn:SetBackdropBorderColor(unpack(ORANGE))

    local label = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    label:SetPoint("CENTER")
    label:SetText(text)
    label:SetTextColor(1, 0.85, 0.6)
    btn.label = label

    btn:SetScript("OnEnter", function(self) self:SetBackdropColor(0.2, 0.1, 0, 1) end)
    btn:SetScript("OnLeave", function(self) self:SetBackdropColor(0, 0, 0, 1) end)

    return btn
end

local throwButton = StyledButton(frame, "Throw", 170)
throwButton:SetPoint("TOP", meterFrame, "BOTTOM", 0, -36)

local newGameButton = StyledButton(frame, "New Game", 150)
newGameButton:SetPoint("RIGHT", throwButton, "LEFT", -16, 0)

local backButton = StyledButton(frame, "Back to Menu", 150)
backButton:SetPoint("LEFT", throwButton, "RIGHT", 16, 0)
backButton:SetScript("OnClick", function()
    meterRunning = false
    frame:Hide()
    RackAndRuin.ShowMenu()
end)

local myName = UnitName("player")

local function TruncateName(name, maxLen)
    if #name > maxLen then
        return name:sub(1, maxLen - 1) .. "…"
    end
    return name
end

-- ============================================================
-- Throw resolution: meter position -> ring -> score
-- ============================================================
local function ResolveThrow(target, pos)
    -- pos: 0 (edge/miss) .. 1 (dead center)
    -- returns: score, label, isFinishEligible (double ring or any bull - the
    -- only throw types allowed to bring a player to exactly 0 and win)
    if target == "BULL" then
        if pos >= 0.97 then return 50, "Inner Bull", true end
        if pos >= 0.90 then return 25, "Outer Bull", true end
        return 0, "Missed the board", false
    end

    if pos >= 0.90 then
        return 0, "Landed in the Bull - missed " .. target, false
    elseif pos >= 0.70 then
        return target * 3, "Triple " .. target, false
    elseif pos >= 0.45 then
        return target * 1, "Single " .. target, false
    elseif pos >= 0.25 then
        return target * 2, "Double " .. target, true
    elseif pos >= 0.10 then
        return target * 1, "Single " .. target, false
    else
        return 0, "Missed the board", false
    end
end

-- ============================================================
-- HOST STATE
-- ============================================================
local host = {
    players = nil,
    scores = nil,        -- name -> remaining score
    turnIndex = nil,
    dartsThisTurn = 0,
    turnStartScore = 0,
    turnThrows = nil,     -- labels thrown this turn, for display
    phase = "idle",       -- idle | playing | done
    statusMsg = "",
}

local function EncodeState()
    local t = {
        phase = host.phase,
        turn = host.players and host.players[host.turnIndex] or "",
        players = table.concat(host.players or {}, ","),
        dartsThisTurn = host.dartsThisTurn,
        status = (host.statusMsg or ""):gsub(" ", "_"),
    }
    for name, score in pairs(host.scores or {}) do
        t["score_" .. name] = score
    end
    return RackAndRuin.Net.Encode(t)
end

local function BroadcastState()
    local payload = EncodeState()
    RackAndRuin.Net.Send(GAME_ID, "STATE", payload)
    RackAndRuin.DARTS_ApplyState(payload)
end

local function HostNextTurn()
    host.turnIndex = host.turnIndex + 1
    if host.turnIndex > #host.players then host.turnIndex = 1 end
    host.dartsThisTurn = 0
    host.turnStartScore = host.scores[host.players[host.turnIndex]]
    host.statusMsg = host.players[host.turnIndex] .. "'s turn"
end

local function HostNewGame()
    host.players = RackAndRuin.Net.GetPlayers()
    host.scores = {}
    for _, name in ipairs(host.players) do
        host.scores[name] = STARTING_SCORE
    end
    host.turnIndex = 1
    host.dartsThisTurn = 0
    host.turnStartScore = STARTING_SCORE
    host.phase = "playing"
    host.statusMsg = host.players[1] .. "'s turn"
    BroadcastState()
end

local function HostProcessThrow(playerName, score, isFinishEligible)
    if host.phase ~= "playing" then return end
    if host.players[host.turnIndex] ~= playerName then return end

    local newScore = host.scores[playerName] - score

    if newScore < 0 then
        -- bust: reset to start-of-turn value, end turn
        host.scores[playerName] = host.turnStartScore
        host.statusMsg = playerName .. " busts!"
        HostNextTurn()
    elseif newScore == 0 and not isFinishEligible then
        -- would finish, but not via a double/bull - dart doesn't count
        host.dartsThisTurn = host.dartsThisTurn + 1
        host.statusMsg = playerName .. " needs a double or bull to finish!"
        if host.dartsThisTurn >= DARTS_PER_TURN then
            HostNextTurn()
        end
    elseif newScore == 0 then
        host.scores[playerName] = 0
        host.phase = "done"
        host.statusMsg = playerName .. " wins with a perfect finish!"
    else
        host.scores[playerName] = newScore
        host.dartsThisTurn = host.dartsThisTurn + 1
        if host.dartsThisTurn >= DARTS_PER_TURN then
            HostNextTurn()
        else
            host.statusMsg = playerName .. "'s turn (" .. (DARTS_PER_TURN - host.dartsThisTurn) .. " darts left)"
        end
    end

    BroadcastState()
end

-- ============================================================
-- CLIENT RENDER
-- ============================================================
function RackAndRuin.DARTS_ApplyState(payload)
    local t = RackAndRuin.Net.Decode(payload)
    if not t.phase or t.phase == "" then return end

    local players = {}
    for name in string.gmatch(t.players or "", "[^,]+") do table.insert(players, name) end

    local lines = {}
    for _, name in ipairs(players) do
        local score = tonumber(t["score_" .. name]) or STARTING_SCORE
        table.insert(lines, TruncateName(name, 10) .. ": " .. score)
    end
    tableText:SetText(table.concat(lines, "\n"))

    statusText:SetText((t.status or ""):gsub("_", " "))

    local dartsLeft = DARTS_PER_TURN - (tonumber(t.dartsThisTurn) or 0)
    if t.phase == "playing" then
        throwsText:SetText("Darts remaining this turn: " .. dartsLeft)
    else
        throwsText:SetText("")
    end

    local myTurn = (t.phase == "playing") and (t.turn == myName)
    if myTurn then
        throwButton:Enable()
        throwButton.label:SetTextColor(1, 0.85, 0.6)
    else
        throwButton:Disable()
        throwButton.label:SetTextColor(0.5, 0.5, 0.5)
        meterRunning = false
    end

    if RackAndRuin.Net.IsHost() then
        newGameButton:Enable()
        newGameButton.label:SetTextColor(1, 0.85, 0.6)
    else
        newGameButton:Disable()
        newGameButton.label:SetTextColor(0.5, 0.5, 0.5)
    end
end

-- ============================================================
-- Network handlers
-- ============================================================
RackAndRuin.Net.RegisterHandler(GAME_ID, "THROW", function(sender, payload)
    if RackAndRuin.Net.IsHost() then
        local scoreStr, flagStr = string.match(payload, "^(%-?%d+),([01])$")
        HostProcessThrow(sender, tonumber(scoreStr) or 0, flagStr == "1")
    end
end)

RackAndRuin.Net.RegisterHandler(GAME_ID, "NEWGAME", function(sender, payload)
    if RackAndRuin.Net.IsHost() then HostNewGame() end
end)

RackAndRuin.Net.RegisterHandler(GAME_ID, "STATE", function(sender, payload)
    RackAndRuin.DARTS_ApplyState(payload)
end)

-- ============================================================
-- Button wiring
-- ============================================================
throwButton:SetScript("OnClick", function()
    if not meterRunning then
        if not selectedTarget then
            statusText:SetText("Pick a target before throwing.")
            return
        end
        meterRunning = true
        meterPos = 0
        meterDir = 1
        throwButton.label:SetText("Lock In!")
        return
    end

    -- lock in the throw
    meterRunning = false
    throwButton.label:SetText("Throw")
    local score, label, isFinishEligible = ResolveThrow(selectedTarget, meterPos)
    statusText:SetText("You hit: " .. label .. " (" .. score .. " points)")

    if RackAndRuin.Net.IsHost() then
        HostProcessThrow(myName, score, isFinishEligible)
    else
        RackAndRuin.Net.Send(GAME_ID, "THROW", tostring(score) .. "," .. (isFinishEligible and "1" or "0"))
    end
end)

newGameButton:SetScript("OnClick", function()
    if RackAndRuin.Net.IsHost() then
        HostNewGame()
    else
        RackAndRuin.Net.Send(GAME_ID, "NEWGAME", "")
    end
end)

function RackAndRuin.ShowDarts()
    frame:Show()
    meterRunning = false
    meterPos = 0
    ClearTargetVisuals()
    selectedTarget = nil
    targetLabel:SetText("Aiming at: -")
    throwButton.label:SetText("Throw")

    if RackAndRuin.Net.IsHost() then
        HostNewGame()
    else
        statusText:SetText("Waiting for host to start...")
        tableText:SetText("")
        throwsText:SetText("")
        throwButton:Disable()
        throwButton.label:SetTextColor(0.5, 0.5, 0.5)
        newGameButton:Disable()
        newGameButton.label:SetTextColor(0.5, 0.5, 0.5)
    end
end

RackAndRuin.DartsFrame = frame
