-- merged_cluster_gforce.lua — cluster UI + G-force polygon dot
-- Auto-generated merge

sim = ac.getSim()

--══════════════════════════════════════════════════════════════════════════════
--  cluster_plus_bars.lua — Combines cluster UI + masked alpha bar + turbo bar
--    • Based on: cluster.lua (live values + UV-cropped alpha-fill mask)
--    • Turbo spool bar is smooth-fill. Oil bar removed.
--  Notes:
--    - Uses a single `update(dt)` entrypoint that draws everything.
--    - Helpers are de-duplicated; shared `clamp`/`rect` etc.
--    - Oil bar code removed; turbo bar uses adjustable 4-corner outline.
--══════════════════════════════════════════════════════════════════════════════

---------------------------------- USER SETTINGS ------------------------------
-- Speed / range
local useMph = false           -- false = km/h, true = mph
local fuelKmsPerLitre = 9      -- km per litre for the range readout

-- Masked alpha bar toggle + settings
local ENABLE_MASKED_ALPHA_BAR = true
local ALPHA_MASK_PATH = "cluster/urus_cluster_mask.png"  -- ← your mask
local BAR_POS  = vec2(0, 0)    -- top-left in the display texture
local BAR_SIZE = vec2(0, 0)    -- pixel size of the mask area
local BAR_FADE = 8             -- soft tip fade (pixels inside the fill)

-- Fraction source for the demo bar (0..1). Here: fuel fraction.
local function alphaBarFraction()
  if car and car.maxFuel and car.maxFuel > 0 then
    return math.max(0, math.min(1, (car.fuel or 0) / car.maxFuel))
  end
  return 0
end


-- Cluster background overlay image
local CLUSTER_BACKGROUND_IMAGE = "cluster/Land rover_Cluster3.png"
local CLUSTER_SIZE = vec2(2048, 2048)

---------------------------------- DISPLAY COLOURS ----------------------------
local white = rgbm(1, 1, 1,1)

---------------------------------- GEAR MAPS ----------------------------------
local gearT = { [-1]="R",[0]="N",[1]="1",[2]="2",[3]="3",[4]="4",[5]="5",[6]="6",[7]="7",[8]="8" }
local gearM = { [0]="N", [1]="D",[2]="D",[3]="D",[4]="4",[5]="5",[6]="6",[7]="7",[8]="8" }

---------------------------------- HELPERS ------------------------------------
local function v(str) local x,y=str:match("([^,]+),%s*([^,]+)"); return vec2(tonumber(x), tonumber(y)) end
local function lerp(a,b,t) return a + (b - a) * t end
local function clamp(val,a,b) return (val<a) and a or (val>b) and b or val end

---------------------------------- RECT DRAW HELPER ---------------------------
local function rect(x,y,w,h,c,opacity)
  display.rect{ pos=vec2(x,y), size=vec2(w,h), color=c, opacity=opacity or 1 }
end

---------------------------------- TURN SIGNALS -------------------------------
local indicatorActive = false

---------------------------------- FALLBACK: RECT-BASED ALPHA FILL -----------
local function drawAlphaFillBar(pos, size, fraction, fadePx)
  local w = math.floor(size.x * clamp(fraction or 0, 0, 1) + 0.5)
  if w <= 0 then return end
  local fade = math.max(tonumber(fadePx) or 0, 0)
  local solidW = math.max(w - fade, 0)
  if solidW > 0 then
    display.rect{ pos = pos, size = vec2(solidW, size.y), color = rgb(0,0,0), opacity = 1 }
  end
  local rest = w - solidW
  if rest > 0 then
    local slices = math.max(math.ceil(rest / 4), 4)
    local segW   = rest / slices
    for i = 0, slices - 1 do
      local a  = (i + 1) / slices
      local sx = pos.x + solidW + math.floor(i * segW + 0.5)
      local sw = math.ceil(segW)
      if sx + sw > pos.x + w then sw = pos.x + w - sx end
      if sw > 0 then
        display.rect{ pos = vec2(sx, pos.y), size = vec2(sw, size.y), color = rgb(0,0,0), opacity = a }
      end
    end
  end
end

---------------------------------- MASK-AWARE (UV-CROPPED) ALPHA FILL ---------
local _maskTex = nil
local function maskTex()
  if _maskTex ~= nil then return _maskTex end
  local ok, tex = pcall(function() return display.loadTexture(ALPHA_MASK_PATH) end)
  _maskTex = ok and tex or false
  return _maskTex
end

local function drawMaskedAlphaFillBar(pos, size, fraction, fadePx)
  local tex = maskTex()
  if not tex then
    return drawAlphaFillBar(pos, size, fraction, fadePx)
  end

  local f = clamp(fraction or 0, 0, 1)
  if f <= 0 then return end

  local filledPx = size.x * f
  display.image{
    texture = tex,
    pos     = pos,
    size    = vec2(filledPx, size.y),
    uvStart = vec2(0, 0),
    uvEnd   = vec2(f, 1),
    color   = rgbm(0, 0, 0, 1)  -- writes alpha only (RGB ignored by material)
  }

  local fade = math.max(tonumber(fadePx) or 0, 0)
  if fade > 0 then
    local fadeW  = math.min(fade, filledPx)
    local slices = math.max(math.ceil(fadeW / 4), 4)
    local segW   = fadeW / slices
    for i = 0, slices - 1 do
      local segPx0 = filledPx - fadeW + i * segW
      local segPx1 = segPx0 + segW
      local uv0 = segPx0 / size.x
      local uv1 = segPx1 / size.x
      local sx = pos.x + segPx0
      local sw = segW
      local a  = 1 - (i + 1) / slices     -- 1 → 0 toward the tip
      display.image{
        texture = tex,
        pos     = vec2(sx, pos.y),
        size    = vec2(sw, size.y),
        uvStart = vec2(uv0, 0),
        uvEnd   = vec2(uv1, 1),
        color   = rgbm(0, 0, 0, a)
      }
    end
  end
end



--══════════════════════════════════════════════════════════════════════════════
--                           SMOOTH WATER + FUEL FILL BARS
--   These draw BEFORE the cluster texture overlay, so the texture sits above
--   the bars. Adjust Start/Size to map the bars behind the cluster artwork.
--══════════════════════════════════════════════════════════════════════════════

local WATER_BAR = {
  Start = "1077.5, 1303.5", 
  Size = "165.5, 24.3",
  Color = rgb(1, 1, 1),
  Direction = "left", -- "left" | "right" | "up" | "down"
  Min = 50,
  Max = 130,
}

local FUEL_BAR = {
 Start = "840.2, 1302.3", 
 Size = "163.6, 24.6",
  Color = rgb(1, 1, 1),
  Direction = "left", -- "left" | "right" | "up" | "down"
}

local function drawSmoothFillBar(cfg, fraction)
  fraction = clamp(fraction or 0, 0, 1)

  local pos  = v(cfg.Start)
  local size = v(cfg.Size)
  local col  = cfg.Color or rgb(1, 1, 1)
  local dir  = cfg.Direction or "left"

  -- Black background behind the exact same bar area
  display.rect({
    pos = pos,
    size = size,
    color = cfg.BackgroundColor or rgb(0.3, 0.3, 0.3),
    opacity = cfg.BackgroundOpacity or 1,
    corner = 0,
    border = 0
  })

  if fraction <= 0 then return end

  if dir == "right" then
    local fillW = size.x * fraction
    display.rect({
      pos = vec2(pos.x + size.x - fillW, pos.y),
      size = vec2(fillW, size.y),
      color = col,
      opacity = cfg.Opacity or 1,
      corner = cfg.Corner or 0,
      border = 0
    })

  elseif dir == "up" then
    local fillH = size.y * fraction
    display.rect({
      pos = vec2(pos.x, pos.y + size.y - fillH),
      size = vec2(size.x, fillH),
      color = col,
      opacity = cfg.Opacity or 1,
      corner = cfg.Corner or 0,
      border = 0
    })

  elseif dir == "down" then
    local fillH = size.y * fraction
    display.rect({
      pos = pos,
      size = vec2(size.x, fillH),
      color = col,
      opacity = cfg.Opacity or 1,
      corner = cfg.Corner or 0,
      border = 0
    })

  else -- "left"
    local fillW = size.x * fraction
    display.rect({
      pos = pos,
      size = vec2(fillW, size.y),
      color = col,
      opacity = cfg.Opacity or 1,
      corner = cfg.Corner or 0,
      border = 0
    })
  end
end

local MASK_DEBUG_SHOW = false
local function debugDrawMask()
  if not MASK_DEBUG_SHOW then return end
  local tex = maskTex()
  if tex then
    display.image{ texture = tex, pos = BAR_POS, size = BAR_SIZE, color = rgbm(1,1,1,0.25) }
  end
end

--══════════════════════════════════════════════════════════════════════════════
--                              SMOOTH TURBO BAR
--   Oil bar removed. Turbo bar uses smooth fill + adjustable outline corners.
--══════════════════════════════════════════════════════════════════════════════

local BAR_TURBO = { x = 330, y = 843, w = 194, h = 7 }

-- Fill direction: "left" | "right" | "both"
local TURBO_FILL_MODE = "left"

-- Smooth fill color
local TURBO_FILL_COL = rgb(1.00, 1.00, 1.00)

-- Outline
local TURBO_DRAW_OUTLINE = false
local OUTLINE_COL = rgb(1, 1, 1)

-- Default = 0.
-- Adjust each corner independently if the turbo outline needs skewing.
local TURBO_BOX_OFFSETS = {
  tl = vec2(0, 0), -- top-left
  tr = vec2(-6, 0), -- top-right
  br = vec2(0, 0), -- bottom-right
  bl = vec2(6, 0), -- bottom-left
}

local function drawLineDots(a, b, color, opacity)
  local dx = b.x - a.x
  local dy = b.y - a.y
  local len = math.sqrt(dx * dx + dy * dy)
  local n = math.max(1, math.floor(len))

  for i = 0, n do
    local t = i / n
    display.rect({
      pos = vec2(a.x + dx * t - 0.5, a.y + dy * t - 0.5),
      size = vec2(1, 1),
      color = color,
      opacity = opacity or 1,
      corner = 0,
      border = 0
    })
  end
end

local function outlineTurboCustom(box)
  if not TURBO_DRAW_OUTLINE then return end

  local tl = vec2(box.x,         box.y)         + TURBO_BOX_OFFSETS.tl
  local tr = vec2(box.x + box.w, box.y)         + TURBO_BOX_OFFSETS.tr
  local br = vec2(box.x + box.w, box.y + box.h) + TURBO_BOX_OFFSETS.br
  local bl = vec2(box.x,         box.y + box.h) + TURBO_BOX_OFFSETS.bl

  drawLineDots(tl, tr, OUTLINE_COL, 1)
  drawLineDots(tr, br, OUTLINE_COL, 1)
  drawLineDots(br, bl, OUTLINE_COL, 1)
  drawLineDots(bl, tl, OUTLINE_COL, 1)
end

local function drawQuadDots(tl, tr, br, bl, color)
  local leftLen  = math.sqrt((bl.x - tl.x)^2 + (bl.y - tl.y)^2)
  local rightLen = math.sqrt((br.x - tr.x)^2 + (br.y - tr.y)^2)
  local rows = math.max(1, math.ceil(math.max(leftLen, rightLen)))

  for y = 0, rows do
    local ty = y / rows

    local l = vec2(
      tl.x + (bl.x - tl.x) * ty,
      tl.y + (bl.y - tl.y) * ty
    )

    local r = vec2(
      tr.x + (br.x - tr.x) * ty,
      tr.y + (br.y - tr.y) * ty
    )

    local dx = r.x - l.x
    local dy = r.y - l.y
    local len = math.sqrt(dx * dx + dy * dy)
    local cols = math.max(1, math.ceil(len))

    for x = 0, cols do
      local tx = x / cols
      display.rect({
        pos = vec2(l.x + dx * tx - 0.5, l.y + dy * tx - 0.5),
        size = vec2(1, 1),
        color = color,
        corner = 0,
        border = 0,
        opacity = 1
      })
    end
  end
end

local function drawTurboSmooth(box, progress)
  progress = clamp(progress or 0, 0, 1)

  local tl = vec2(box.x,         box.y)         + TURBO_BOX_OFFSETS.tl
  local tr = vec2(box.x + box.w, box.y)         + TURBO_BOX_OFFSETS.tr
  local br = vec2(box.x + box.w, box.y + box.h) + TURBO_BOX_OFFSETS.br
  local bl = vec2(box.x,         box.y + box.h) + TURBO_BOX_OFFSETS.bl

  if TURBO_FILL_MODE == "both" then
    local midTop = vec2((tl.x + tr.x) * 0.5, (tl.y + tr.y) * 0.5)
    local midBot = vec2((bl.x + br.x) * 0.5, (bl.y + br.y) * 0.5)

    local leftTop = vec2(midTop.x + (tl.x - midTop.x) * progress, midTop.y + (tl.y - midTop.y) * progress)
    local leftBot = vec2(midBot.x + (bl.x - midBot.x) * progress, midBot.y + (bl.y - midBot.y) * progress)

    local rightTop = vec2(midTop.x + (tr.x - midTop.x) * progress, midTop.y + (tr.y - midTop.y) * progress)
    local rightBot = vec2(midBot.x + (br.x - midBot.x) * progress, midBot.y + (br.y - midBot.y) * progress)

    drawQuadDots(leftTop, midTop, midBot, leftBot, TURBO_FILL_COL)
    drawQuadDots(midTop, rightTop, rightBot, midBot, TURBO_FILL_COL)

  elseif TURBO_FILL_MODE == "right" then
    local fillTop = vec2(tr.x + (tl.x - tr.x) * progress, tr.y + (tl.y - tr.y) * progress)
    local fillBot = vec2(br.x + (bl.x - br.x) * progress, br.y + (bl.y - br.y) * progress)

    drawQuadDots(fillTop, tr, br, fillBot, TURBO_FILL_COL)

  else
    local fillTop = vec2(tl.x + (tr.x - tl.x) * progress, tl.y + (tr.y - tl.y) * progress)
    local fillBot = vec2(bl.x + (br.x - bl.x) * progress, bl.y + (br.y - bl.y) * progress)

    drawQuadDots(tl, fillTop, fillBot, bl, TURBO_FILL_COL)
  end

  outlineTurboCustom(box)
end

local function getTurboFrac()
  local f = 0

  if car then
    if car.turboBoost ~= nil then
      f = car.turboBoost
    elseif car.boostRatio ~= nil then
      f = car.boostRatio
    elseif car.boostPressure ~= nil and car.maxBoostPressure ~= nil then
      f = car.boostPressure / math.max(0.001, car.maxBoostPressure)
    end
  end

  return clamp(f, 0, 1)
end

--══════════════════════════════════════════════════════════════════════════════
--                                   UPDATE
--══════════════════════════════════════════════════════════════════════════════










--══════════════════════════════════════════════════════════════════════════════
--                         POWER + TORQUE VERTICAL GRAPH
--   One graph only. Newest sample on the right. Normal vertical graph.
--   0% = bottom, 100% = top. Power = white, Torque = red.
--══════════════════════════════════════════════════════════════════════════════

--──────────────────────────────────────────────────────────────────────────────
--  RPM CURVE POINTS  ← edit these to match your car's dyno sheet
--
--  Each row: { rpm, hp, torque_Nm }
--  Keep rows sorted low → high RPM.
--  Peak hp and peak tq are detected automatically — just enter raw dyno numbers.
--  Add or remove rows freely (minimum 2 points).
--──────────────────────────────────────────────────────────────────────────────
local RPM_CURVE = {
  -- { rpm,   hp,   tq(Nm) }
  {  1000,   30,  242 },
  {  2500,  124,  350 },
  {  4000,  400,  600 },
  {  5500,  550,  700 },
  {  6500,  557,  615 },   -- peak power near here
  {  7300,  479,  467 },
}

-- Pre-compute peaks so 100% on the graph = actual peak value
local CURVE_PEAK_HP = 1
local CURVE_PEAK_TQ = 1
for _, pt in ipairs(RPM_CURVE) do
  if pt[2] > CURVE_PEAK_HP then CURVE_PEAK_HP = pt[2] end
  if pt[3] > CURVE_PEAK_TQ then CURVE_PEAK_TQ = pt[3] end
end

-- Linear interpolation across the curve points at a given RPM
local function interpolateCurve(rpm)
  local pts = RPM_CURVE
  if #pts == 0 then return 0, 0 end
  if rpm <= pts[1][1] then
    local frac = math.max(rpm, 0) / math.max(pts[1][1], 1)
    return pts[1][2] * frac, pts[1][3] * frac
  end
  if rpm >= pts[#pts][1] then
    return pts[#pts][2], pts[#pts][3]
  end
  for i = 1, #pts - 1 do
    local lo, hi = pts[i], pts[i + 1]
    if rpm >= lo[1] and rpm <= hi[1] then
      local t = (rpm - lo[1]) / math.max(hi[1] - lo[1], 1)
      return lo[2] + (hi[2] - lo[2]) * t,
             lo[3] + (hi[3] - lo[3]) * t
    end
  end
  return 0, 0
end

local POWER_TORQUE_GRAPH_ENABLED = true

-- Move/resize graph here
local POWER_TORQUE_GRAPH_BOX = {
Start = "276.8, 891.1",
Size = "310, 209.4",
}

-- Visuals
local POWER_TORQUE_GRAPH_SHOW_BG    = false
local POWER_TORQUE_GRAPH_SHOW_FRAME = false

local POWER_GRAPH_COLOR  = rgbm(1, 1, 1, 0.5)  -- power = white
local TORQUE_GRAPH_COLOR = rgb(1, 0.05, 0.05)        -- torque = red
local POWER_TORQUE_GRAPH_BG_COLOR    = rgb(0, 0, 0)
local POWER_TORQUE_GRAPH_FRAME_COLOR = rgb(1, 1, 1)

local POWER_TORQUE_GRAPH_BG_OPACITY = 0
local POWER_TORQUE_GRAPH_HISTORY    = 150
local POWER_TORQUE_GRAPH_BAR_W      = 2
local POWER_TORQUE_GRAPH_GAP        = 0
-- Gradient quality: slices per column. More = smoother fade, slightly heavier.
-- Recommended range: 16 (fast/coarse) to 48 (smooth). Default 28.
local POWER_TORQUE_GRAPH_SLICES     = 38
-- Minimum fraction to draw anything. Values below this are treated as zero.
-- Raise slightly (e.g. 0.02) if a faint line persists at idle.
local POWER_TORQUE_GRAPH_MIN_FRAC   = 0.01

-- Smooths the live input before it gets pushed into the graph.
-- Higher = reacts faster, lower = smoother/slower. Try 4-10.
local POWER_TORQUE_GRAPH_SMOOTH_HZ = 1

-- Debug: animates graph if car values are 0, useful for positioning
local POWER_TORQUE_GRAPH_DEBUG_MODE = false

local powerGraphHistory  = {}
local torqueGraphHistory = {}
local powerTorqueGraphTime = 0
local powerGraphSmooth = 0
local torqueGraphSmooth = 0

local function graphPush(history, value)
  table.insert(history, 1, clamp(value or 0, 0, 1))
  while #history > POWER_TORQUE_GRAPH_HISTORY do
    table.remove(history)
  end
end

local function smoothValue(current, target, dt, hz)
  target = clamp(target or 0, 0, 1)
  current = current or target
  local rate = math.max(hz or 0, 0.001)
  local t = 1 - math.exp(-rate * math.max(dt or 0, 0))
  return current + (target - current) * t
end

-- Draw one column of the graph with a gradient fade and red/white blend.
-- Power (white) and torque (red) share the same column — where they overlap
-- they mix into a warm pink/salmon, mimicking the RR SV cluster look.
local function drawGraphColumn(x, graphTop, graphH, pFrac, tFrac)
  -- Hard cutoff — skip entirely below the minimum threshold so no phantom
  -- line lingers at the bottom after RPM drops to zero.
  local minF = POWER_TORQUE_GRAPH_MIN_FRAC
  pFrac = (pFrac or 0) > minF and clamp(pFrac, 0, 1) or 0
  tFrac = (tFrac or 0) > minF and clamp(tFrac, 0, 1) or 0
  if pFrac <= 0 and tFrac <= 0 then return end

  local w      = POWER_TORQUE_GRAPH_BAR_W
  local slices = math.max(POWER_TORQUE_GRAPH_SLICES, 4)
  local sliceH = graphH / slices

  for s = 1, slices do
    local vBot = (s - 1) / slices
    local vTop = s       / slices

    -- Skip slices entirely above both bars — nothing to draw up there
    if vBot >= pFrac and vBot >= tFrac then break end

    -- Each line fades from opaque at the tip to transparent at the base.
    -- Power ^ 1.8 keeps whites vivid near the tip; torque ^ 1.4 slightly wider glow.
    local pAlpha, tAlpha = 0, 0
    if vBot < pFrac then
      pAlpha = vTop < pFrac
        and (vTop / math.max(pFrac, 0.001)) ^ 1.8
        or  1.0
    end
    if vBot < tFrac then
      tAlpha = vTop < tFrac
        and (vTop / math.max(tFrac, 0.001)) ^ 1.4
        or  1.0
    end

    -- Hard alpha floor — discard near-invisible slices that cause the ghost line
    if pAlpha < 0.01 and tAlpha < 0.01 then goto continue end

    -- Blend: red+white overlap produces a warm desaturated pink
    local r = math.min(1, tAlpha + pAlpha)
    local g = math.min(1, pAlpha * 0.9)
    local b = math.min(1, pAlpha * 0.9)
    local a = math.min(1, tAlpha * 0.85 + pAlpha * 0.6)

    display.rect({
      pos     = vec2(x, graphTop + graphH - (s * sliceH)),
      size    = vec2(w, sliceH + 0.5),
      color   = rgbm(r, g, b, a),
      opacity = 1,
      border  = 0,
      corner  = 0
    })
    ::continue::
  end
end

local function drawPowerTorqueGraph(dt)
  if not POWER_TORQUE_GRAPH_ENABLED then return end

  dt = type(dt) == "number" and dt or 1 / 60
  powerTorqueGraphTime = powerTorqueGraphTime + dt

  local powerFrac = 0
  local torqueFrac = 0

  if car then
    local rpm = car.rpm or 0
    local gas = clamp(car.gas or 0, 0, 1)

    -- Look up hp and tq at current RPM from the curve table,
    -- then scale by throttle (0 = coast/no load, 1 = full throttle WOT).
    local hp, tq = interpolateCurve(rpm)
    powerFrac  = clamp((hp  * gas) / math.max(CURVE_PEAK_HP, 1), 0, 1)
    torqueFrac = clamp((tq  * gas) / math.max(CURVE_PEAK_TQ, 1), 0, 1)
  end

  powerFrac  = clamp(powerFrac,  0, 1)
  torqueFrac = clamp(torqueFrac, 0, 1)

  if POWER_TORQUE_GRAPH_DEBUG_MODE then
    if powerFrac <= 0.001 then
      powerFrac = 0.5 + 0.45 * math.sin(powerTorqueGraphTime * 2.0)
    end
    if torqueFrac <= 0.001 then
      torqueFrac = 0.5 + 0.40 * math.sin(powerTorqueGraphTime * 2.7 + 1.2)
    end
  end

  powerGraphSmooth  = smoothValue(powerGraphSmooth,  powerFrac,  dt, POWER_TORQUE_GRAPH_SMOOTH_HZ)
  torqueGraphSmooth = smoothValue(torqueGraphSmooth, torqueFrac, dt, POWER_TORQUE_GRAPH_SMOOTH_HZ)

  graphPush(powerGraphHistory, powerGraphSmooth)
  graphPush(torqueGraphHistory, torqueGraphSmooth)

  local graphPos  = v(POWER_TORQUE_GRAPH_BOX.Start)
  local graphSize = v(POWER_TORQUE_GRAPH_BOX.Size)

  if POWER_TORQUE_GRAPH_SHOW_BG then
    display.rect({
      pos = graphPos,
      size = graphSize,
      color = POWER_TORQUE_GRAPH_BG_COLOR,
      opacity = POWER_TORQUE_GRAPH_BG_OPACITY,
      border = 0,
      corner = 0
    })
  end

  if POWER_TORQUE_GRAPH_SHOW_FRAME then
    display.rect({ pos = graphPos, size = vec2(graphSize.x, 1), color = POWER_TORQUE_GRAPH_FRAME_COLOR, opacity = 1, border = 0, corner = 0 })
    display.rect({ pos = vec2(graphPos.x, graphPos.y + graphSize.y - 1), size = vec2(graphSize.x, 1), color = POWER_TORQUE_GRAPH_FRAME_COLOR, opacity = 1, border = 0, corner = 0 })
    display.rect({ pos = graphPos, size = vec2(1, graphSize.y), color = POWER_TORQUE_GRAPH_FRAME_COLOR, opacity = 1, border = 0, corner = 0 })
    display.rect({ pos = vec2(graphPos.x + graphSize.x - 1, graphPos.y), size = vec2(1, graphSize.y), color = POWER_TORQUE_GRAPH_FRAME_COLOR, opacity = 1, border = 0, corner = 0 })
  end

  local sampleStep = POWER_TORQUE_GRAPH_BAR_W + POWER_TORQUE_GRAPH_GAP
  local maxSamplesVisible = math.floor(graphSize.x / sampleStep)

  for i = 1, math.min(#powerGraphHistory, maxSamplesVisible) do
    local x = graphPos.x + graphSize.x - (i * sampleStep)
    drawGraphColumn(x, graphPos.y, graphSize.y,
      powerGraphHistory[i]  or 0,
      torqueGraphHistory[i] or 0)
  end
end


local function cluster_update(dt)
  -- ── RPM gauge ────────────────────────────────────────────────────────────────────
  local RPM_MAX        = 7200           -- RPM at which gauge is full
  local RPM_SQUARES    = 16             -- number of segments
  local RPM_DEG_STEP   = 2.402          -- degrees per 1% of RPM
  local RPM_ARC_OFFSET = 130            -- arc start offset (degrees)
  local RPM_INNER_ROT  = 2              -- inner tilt of each square (degrees)

  -- Square color — hard switches at set RPM thresholds, no blending:
  local RPM_COLOR_NORMAL = rgbm(.65, .65, .65, 1)  -- below RPM_COLOR1_RPM
  local RPM_COLOR1_RPM   = 6250                     -- switches to orange at this RPM
  local RPM_COLOR1       = rgbm(1, 0.55, 0,   1)   -- yellow/orange
  local RPM_COLOR2_RPM   = 7000                     -- switches to red at this RPM
  local RPM_COLOR2       = rgbm(1, 0,    0,   1)   -- red

  -- ── RPM background box ────────────────────────────────────────────────────────────
  local RPM_BG_ENABLED = true
  local RPM_BG_POS     = vec2(9, 405)
  local RPM_BG_SIZE    = vec2(1800, 990)
  local RPM_BG_COLOR   = rgbm(0.15, 0.15, 0.15, 1)

  -- ── RPM debug ────────────────────────────────────────────────────────────────────
  local RPM_DEBUG     = false   -- freeze gauge at a fixed RPM for positioning
  local RPM_DEBUG_RPM = 7200

  -- ── DRAW LAYER 1: RPM gauge ───────────────────────────────────────────────────────
  do
    local liveRpm       = RPM_DEBUG and RPM_DEBUG_RPM or (car.rpm or 0)
    local rpmPercentage = liveRpm / RPM_MAX * 100

    if RPM_BG_ENABLED then
      display.rect{ pos = RPM_BG_POS, size = RPM_BG_SIZE, color = RPM_BG_COLOR }
    end

    local amountOfSquares = clamp(math.ceil(rpmPercentage / (100 / RPM_SQUARES)), 0, RPM_SQUARES)

    -- Hard color switch at each RPM threshold — no blending
    local squareColor = liveRpm >= RPM_COLOR2_RPM and RPM_COLOR2
                     or liveRpm >= RPM_COLOR1_RPM and RPM_COLOR1
                     or RPM_COLOR_NORMAL

    local rpmPos   = vec2(750, 1050)
    local rpmSize  = vec2(200, 200)
    local rpmPivot = vec2(1039, 1039)

    for i = 1, amountOfSquares do
      local thisRotation = (-rpmPercentage) * RPM_DEG_STEP
      ui.beginRotation()
      ui.beginRotation()
      display.rect{ pos = rpmPos, size = rpmSize, color = squareColor }
      ui.endRotation(RPM_INNER_ROT)
      if rpmPercentage > (100 / RPM_SQUARES * i) then
        thisRotation = -(100 / RPM_SQUARES * i) * RPM_DEG_STEP
      end
      ui.endPivotRotation(thisRotation + RPM_ARC_OFFSET, rpmPivot)
    end
  end

  -- ── DRAW LAYER 2: water + fuel bars ───────────────────────────────────────────────
  local preWaterC = car.waterTemperature or 0
  local preWaterFrac = clamp((preWaterC - WATER_BAR.Min) / math.max(1, (WATER_BAR.Max - WATER_BAR.Min)), 0, 1)
  drawSmoothFillBar(WATER_BAR, preWaterFrac)

  local preFuelFrac = 0
  if car and car.maxFuel and car.maxFuel > 0 then
    preFuelFrac = clamp((car.fuel or 0) / car.maxFuel, 0, 1)
  end
  drawSmoothFillBar(FUEL_BAR, preFuelFrac)

  -- ── DRAW LAYER 3: cluster texture ──────────────────────────────────────────────────
  display.image{ image = CLUSTER_BACKGROUND_IMAGE, pos = vec2(0, 0), size = CLUSTER_SIZE }


  -- Live values
  local speedKmh = car.speedKmh
  local speedVal = useMph and (speedKmh / 1.609344) or speedKmh

  local timeText   = string.format("%02d:%02d", sim.timeHours, sim.timeMinutes)
  local timeText2  = string.format("%02d:%02d", sim.timeHours, sim.timeMinutes)
  local speedText  = tostring(math.floor(speedVal))
  local tempText   = string.format("%.f", sim.ambientTemperature)
  local gearText   = gearT[car.gear]
  local gearText2  = gearM[car.gear]
  local rangeText  = tostring(math.floor(car.fuel * fuelKmsPerLitre))
  local totalText  = string.format("%06d", car.distanceDrivenTotalKm)
  local sessText   = string.format("%1d", car.distanceDrivenSessionKm)
  local waterTemp  = string.format("%1d", car.waterTemperature)

-- 🕒 TIME — split positioning (minimal)

local TIME_HOURS_POS   = v("685.9, 714.4")
local TIME_COLON_POS   = v("716.0, 713.4")
local TIME_MINUTES_POS = v("728.0, 714.4")

local timeHours   = string.format("%02d", sim.timeHours)
local timeMinutes = string.format("%02d", sim.timeMinutes)

display.text({
  text = timeHours,
  pos = TIME_HOURS_POS,
  letter = v("22, 60"),
  font = "fonts/helvetica-med",
  color = white,
  alignment = 0.9,
  width = 0,
  spacing = -5
})

display.text({
  text = ":",
  pos = TIME_COLON_POS,
  letter = v("22, 60"),
  font = "fonts/helvetica-med",
  color = white,
  alignment = 0.9,
  width = 0,
  spacing = -1
})

display.text({
  text = timeMinutes,
  pos = TIME_MINUTES_POS,
  letter = v("22, 60"),
  font = "fonts/helvetica-med",
  color = white,
  alignment = 0.9,
  width = 0,
  spacing = -5
})

  -- 🚗 Gear — true numeric gears (1-7): white outline + red fill + "S" badge
  --           R and N: plain white only
  local isNumericGear = car.gear >= 1 and car.gear <= 7

  if isNumericGear then
    -- White outline (8 passes)
    display.text({ text = gearText, pos = v("922, 900"), letter = v("80,230"),
      font= "fonts/helvetica-med", color = white, alignment = .5, width = 255, spacing = -15 })
    display.text({ text = gearText, pos = v("910, 900"), letter = v("80,230"),
      font= "fonts/helvetica-med", color = white, alignment = .5, width = 255, spacing = -15 })
    display.text({ text = gearText, pos = v("916, 906"), letter = v("80,230"),
      font= "fonts/helvetica-med", color = white, alignment = .5, width = 255, spacing = -15 })
    display.text({ text = gearText, pos = v("916, 894"), letter = v("80,230"),
      font= "fonts/helvetica-med", color = white, alignment = .5, width = 255, spacing = -15 })
    display.text({ text = gearText, pos = v("920, 904"), letter = v("80,230"),
      font= "fonts/helvetica-med", color = white, alignment = .5, width = 255, spacing = -15 })
    display.text({ text = gearText, pos = v("912, 896"), letter = v("80,230"),
      font= "fonts/helvetica-med", color = white, alignment = .5, width = 255, spacing = -15 })
    display.text({ text = gearText, pos = v("920, 896"), letter = v("80,230"),
      font= "fonts/helvetica-med", color = white, alignment = .5, width = 255, spacing = -15 })
    display.text({ text = gearText, pos = v("912, 904"), letter = v("80,230"),
      font= "fonts/helvetica-med", color = white, alignment = .5, width = 255, spacing = -15 })
    -- Red fill on top
    display.text({ text = gearText, pos = v("916, 900"), letter = v("80,230"),
      font= "fonts/helvetica-med", color = rgb(1, 0, 0), alignment = .5, width = 255, spacing = -15 })

    -- "S" badge — white outline + red fill (position/size tweakable below)
    local S_POS    = v("910, 970")   -- position of the S badge
    local S_LETTER = v("30, 80")     -- size of the S badge
    -- White outline (4 passes, tighter radius for small text)
    display.text({ text = "S", pos = vec2(S_POS.x + 3, S_POS.y), letter = S_LETTER,
      font= "fonts/helvetica-med", color = white, alignment = .5, width = 100, spacing = 0 })
    display.text({ text = "S", pos = vec2(S_POS.x - 3, S_POS.y), letter = S_LETTER,
      font= "fonts/helvetica-med", color = white, alignment = .5, width = 100, spacing = 0 })
    display.text({ text = "S", pos = vec2(S_POS.x, S_POS.y + 3), letter = S_LETTER,
      font= "fonts/helvetica-med", color = white, alignment = .5, width = 100, spacing = 0 })
    display.text({ text = "S", pos = vec2(S_POS.x, S_POS.y - 3), letter = S_LETTER,
      font= "fonts/helvetica-med", color = white, alignment = .5, width = 100, spacing = 0 })
    -- Red fill
    display.text({ text = "S", pos = S_POS, letter = S_LETTER,
      font= "fonts/helvetica-med", color = rgb(1, 0, 0), alignment = .5, width = 100, spacing = 0 })
  else
    -- R / N — plain white, no outline, no S badge
    display.text({ text = gearText, pos = v("916, 900"), letter = v("80,230"),
      font= "fonts/helvetica-med", color = white, alignment = .5, width = 255, spacing = -15 })
  end

  -- 🚗 SPEED
  display.text({ text = speedText, pos = v("916, 1033"), letter = v("70,170"),
    font = "fonts/helvetica-med", color = white, alignment = .5, width = 255, spacing = -15 })

  -- 📏 TOTAL KM
  display.text({ text = totalText, pos = v("1268, 1298.4"), letter = v("18.5, 55"),
    font = "fonts/helvetica-med", color = white, alignment = 1, width = 250, spacing = -2.4 })

  -- 📏 SESSION KM
  display.text({ text = sessText,pos = v("400, 1298.4"), letter = v("22.5, 55"),
    font = "fonts/helvetica-med", color = white, alignment = 1, width = 250, spacing = -1 })

  -- 📏 Temp KM
  display.text({ text = tempText, pos = v("1130, 714"), letter = v("22.5, 55"),
    font = "fonts/helvetica-med", color = white, alignment = 1, width = 250, spacing = -4 })



  -- Hide PARK symbol when handbrake released
  if car.handbrake < 0.1 then
    display.rect({ pos = vec2(1894.2, 1306.6), size = vec2(85.0, 85.0),
      color = rgb(0,0,0), corner = 0, border = 0, opacity = 1 })
  end




  --═══════════════════════ DRAW: Masked alpha bar (fuel by default) ══════════
  if ENABLE_MASKED_ALPHA_BAR then
    drawMaskedAlphaFillBar(BAR_POS, BAR_SIZE, alphaBarFraction(), BAR_FADE)
    debugDrawMask()
  end

  --═══════════════════════ DRAW: Turbo spool bar (smooth) ════════════════════
  local turboF = getTurboFrac()
  drawTurboSmooth(BAR_TURBO, turboF)
  -- Power/Torque vertical graph
  drawPowerTorqueGraph(dt)

end


-- g_force_circle.lua — G-meter with circular clamp + textured dot

local DOT_TEXTURE = "cluster/g_force_dot_white.png"
local DOT_SIZE    = vec2(50, 50)

local CENTER = vec2(1614.3, 1001.5)

-- Circle settings
local MAX_G      = 1.0
local RADIUS_1G  = 170
local RADIUS_05G = RADIUS_1G * 0.51
local RADIUS_MAX = RADIUS_1G * MAX_G

local ANGLE_OFFSET = math.pi * 0
local LAG = 0.92
local EMA_HZ = 7.5

-- Debug outlines
local SHOW_DEBUG_OUTLINES = false
local OUTLINE_ALPHA_05G   = 0.25
local OUTLINE_ALPHA_1G    = 0.4
local OUTLINE_DOTS        = 160

GF_SmoothedAccel = { x = 0, z = 0 }

local function ema(prev, target, hz, dt)
  dt = dt or (sim and sim.dt) or 1/60
  if hz <= 0 then return target end
  local k = math.exp(-hz * dt)
  return target + (prev - target) * k
end

local function gf_drawCircleOutline(center, radius, opacity)
  local prev = nil

  for i = 0, OUTLINE_DOTS do
    local a = (i / OUTLINE_DOTS) * math.pi * 2
    local p = vec2(
      center.x + math.cos(a) * radius,
      center.y + math.sin(a) * radius
    )

    display.rect({
      pos = vec2(p.x - 1, p.y - 1),
      size = vec2(2, 2),
      color = rgb(1, 1, 1),
      opacity = opacity,
      corner = 1
    })

    prev = p
  end
end

local function gforce_update(dt)
  if math and math.applyLag and sim and sim.dt then
    GF_SmoothedAccel.x = math.applyLag(GF_SmoothedAccel.x, car.acceleration.x or 0, LAG, sim.dt)
    GF_SmoothedAccel.z = math.applyLag(GF_SmoothedAccel.z, car.acceleration.z or 0, LAG, sim.dt)
  else
    GF_SmoothedAccel.x = ema(GF_SmoothedAccel.x, car.acceleration.x or 0, EMA_HZ, dt)
    GF_SmoothedAccel.z = ema(GF_SmoothedAccel.z, car.acceleration.z or 0, EMA_HZ, dt)
  end

  local gx = GF_SmoothedAccel.x
  local gz = GF_SmoothedAccel.z

  local ang = math.atan2(gz, gx) + ANGLE_OFFSET
  local gMag = math.sqrt(gx * gx + gz * gz)

  -- Clamp to max G circle
  local clampedG = clamp(gMag, 0, MAX_G)
  local radius = (clampedG / MAX_G) * RADIUS_MAX

  local pos = vec2(
    CENTER.x + math.cos(ang) * radius,
    CENTER.y + math.sin(ang) * radius
  )

  -- Debug outlines
  if SHOW_DEBUG_OUTLINES then
    gf_drawCircleOutline(CENTER, RADIUS_05G, OUTLINE_ALPHA_05G)
    gf_drawCircleOutline(CENTER, RADIUS_1G, OUTLINE_ALPHA_1G)
  end

  -- Dot
  display.image({
    image = DOT_TEXTURE,
    pos = vec2(pos.x - DOT_SIZE.x * 0.5, pos.y - DOT_SIZE.y * 0.5),
    size = DOT_SIZE
  })

  -- Right G
  display.text({
    text = string.format("%.1f", math.max(GF_SmoothedAccel.x, 0)),
    pos = vec2(1788, 988),
    letter = vec2(15, 40),
    font = "fonts/helvetica-med",
    width = 46,
    alignment = 1,
    spacing = -5,
    color = rgbm(1, 1, 1, 1)
  })

  -- Left G
  display.text({
    text = string.format("%.1f", math.max(-GF_SmoothedAccel.x, 0)),
    pos = vec2(1385, 988),
    letter = vec2(15, 40),
    font = "fonts/helvetica-med",
    width = 46,
    alignment = 1,
    spacing = -5,
    color = rgbm(1, 1, 1, 1)
  })


  -- Forward G
  display.text({
    text = string.format("%.1f", math.max(-GF_SmoothedAccel.z, 0)),
    pos = vec2(1585, 792),
    letter = vec2(15, 40),
    font = "fonts/helvetica-med",
    width = 46,
    alignment = 1,
    spacing = -5,
    color = rgbm(1, 1, 1, 1)
  })
  -- Rear G
  display.text({
    text = string.format("%.1f", math.max(GF_SmoothedAccel.z, 0)),
    pos = vec2(1585, 1180),
    letter = vec2(15, 40),
    font = "fonts/helvetica-med",
    width = 46,
    alignment = 1,
    spacing = -5,
    color = rgbm(1, 1, 1, 1)
  })
end


function update(dt)
  cluster_update(dt)
  gforce_update(dt)
end
