local sin, cos, sqrt, abs, flr, min, max, ceil =
  sin, cos, sqrt, abs, flr, min, max, math.ceil
local _tline3d = tline3d

-- deterministic RNG for shake/glitch
local _rng = 0x12345678
local function irnd(n)
  _rng = (_rng * 1103515245 + 12345) & 0x7fffffff
  return _rng % n
end

-- screen dimensions (vid 3 = 240x135)
local W = 240
local H = 135
local CX = W \ 2
local CY = H \ 2

-- text spritesheet layout
local LINE_H = 8
local TXT_SCALE = 2

local function clamp(x, a, b)
  if x < a then return a end
  if x > b then return b end
  return x
end

local function smoothstep01(t)
  t = clamp(t, 0, 1)
  return t * t * (3 - 2 * t)
end

local function fade_color(c, fade)
  local r = (c >> 16) & 0xff
  local g = (c >> 8) & 0xff
  local b = c & 0xff
  if fade < 1.0 then
    r = r * fade; g = g * fade; b = b * fade
  else
    local ft = fade - 1.0
    r = r + (255 - r) * ft; g = g + (255 - g) * ft; b = b + (255 - b) * ft
  end
  return (flr(r) << 16) | (flr(g) << 8) | flr(b)
end


-----------------------------
-- RIBBON INTERNALS
-----------------------------

local TAU = 6.28318530718

local RIB = {
  ribbons = 9, knots = 5, subdiv = 4,
  znear = -24, zfar = 80, focal = 120,
  flow = 2.0, spin = 0.35, twist = 0.008,
  helix_r = 21.0, helix_wob = 16.6,
  path_ax = 7.0, path_bx = 1.0,
  path_ay = 7.0, path_by = 1.0,
  ribbon_w = 4.55, spread = 3.0,
  clip_z = 0.0, taper_segs = 4, max_hw = 160.0,
}

local RIB_CLIP_Z = RIB.clip_z
local RIB_ZFAR   = RIB.zfar
local RIB_W_VAL  = RIB.ribbon_w
local RIB_MAX_HW = RIB.max_hw

-- dynamic ribbon params (updated from rocket each frame)
local rib_dyn_spread  = RIB.spread
local rib_dyn_helix_r = RIB.helix_r
local rib_dyn_wob     = RIB.helix_wob
local rib_dyn_focal   = RIB.focal
local rib_dyn_spin    = RIB.spin
local rib_dyn_twist   = RIB.twist
local rib_dyn_cam_x   = 0
local rib_dyn_cam_y   = 0

local RIB_POINTS_PER = (RIB.knots - 3) * RIB.subdiv + 1
local RIB_SEGMENTS   = RIB_POINTS_PER - 1
local RIB_TOTAL_PTS  = RIB_POINTS_PER * RIB.ribbons
local RIB_MAX_QUADS  = RIB.ribbons * RIB_SEGMENTS + 16

local RIB_TEXW = 32
local RIB_TEXH = 32

local rib_time_acc = 0
local rib_bw0, rib_bw1, rib_bw2, rib_bw3 = {}, {}, {}, {}
local rib_FZ, rib_FSCALE = {}, {}
local rib_fx, rib_fy, rib_fz_off = {}, {}, {}
local rib_psx, rib_psy, rib_psc = {}, {}, {}
local rib_quad_zmid, rib_quad_order = {}, {}
local rib_tex
local rib_tex_levels = {}
local RIB_FLASH_LEVELS = 8

local rib_ptxl, rib_ptyl, rib_ptxr, rib_ptyr = {}, {}, {}, {}
local rib_q_x0l, rib_q_y0l, rib_q_x0r, rib_q_y0r = {}, {}, {}, {}
local rib_q_x1l, rib_q_y1l, rib_q_x1r, rib_q_y1r = {}, {}, {}, {}
local rib_q_v0, rib_q_v1 = {}, {}
local rib_q_rid = {}
local rib_qvx, rib_qvy, rib_qvu, rib_qvv = {0,0,0,0}, {0,0,0,0}, {0,0,0,0}, {0,0,0,0}

-- pre-allocated scratch for rib_draw_quad (avoids ~700 table allocs/frame)
local rdq_ix   = {1,2,3,4}
local rdq_eymin = {0,0,0,0}
local rdq_eymax = {0,0,0,0}
local rdq_xbase = {0,0,0,0}
local rdq_xstep = {0,0,0,0}
local rdq_ubase = {0,0,0,0}
local rdq_ustep = {0,0,0,0}
local rdq_vbase = {0,0,0,0}
local rdq_vstep = {0,0,0,0}

-- pre-allocated scratch for rib_compute_paths (avoids 18 table allocs/frame)
local rib_kx     = {}
local rib_ky     = {}
local rib_kz_off_k = {}

-- pre-allocated scratch for draw() sorting/flashing
local rib_layer_z_arr   = {}
local rib_draw_order_arr = {}
local rib_levels_arr     = {}

-- pre-allocated scratch for rib_precompute_z
local rib_knot_z = {}
local rib_last_focal = nil

-- flash state (deterministic, based on t)
local RIB_FLASH_DUR = 0.8
local RIB_FLASH_BASE_PERIOD = 0.6

-- background flash state
local bg_flash_next = 0.1
local bg_flash_start = -99
local bg_flash_dur = 0.3
local BG_FLASH_MIN_GAP = 1.0
local BG_FLASH_MAX_GAP = 2.0
local BG_FLASH_MIN_DUR = 0.15
local BG_FLASH_MAX_DUR = 0.4
local BG_FLASH_SHIFT  = 20
local bg_base_pal = {}


local function rib_build_texture()
  local tex = userdata("u8", RIB_TEXW, RIB_TEXH)
  for y=0,RIB_TEXH-1 do
    local row_bright = (y + 0.5) / RIB_TEXH
    for x=0,RIB_TEXW-1 do
      local cx = abs(((x + 0.5) / RIB_TEXW) * 2 - 1)
      local edge_dim = 1.0 - cx * cx
      local brightness = row_bright * edge_dim
      local c = flr(brightness * 30 + 1)
      if c < 1 then c = 1 end
      if c > 31 then c = 31 end
      tex:set(x, y, c)
    end
  end
  rib_tex = tex

  for lvl=1,RIB_FLASH_LEVELS do
    local mix = lvl / RIB_FLASH_LEVELS
    local ltex = userdata("u8", RIB_TEXW, RIB_TEXH)
    for y=0,RIB_TEXH-1 do
      local row_bright = (y + 0.5) / RIB_TEXH
      for x=0,RIB_TEXW-1 do
        local cx = abs(((x + 0.5) / RIB_TEXW) * 2 - 1)
        local edge_dim = 1.0 - cx * cx
        local brightness = row_bright * edge_dim
        local cn = flr(brightness * 30 + 1)
        if cn < 1 then cn = 1 end
        if cn > 31 then cn = 31 end
        local cf = flr(brightness * 15 + 32)
        if cf < 32 then cf = 32 end
        if cf > 47 then cf = 47 end
        local c
        if brightness < 0.05 then
          c = cn
        elseif mix > 0.5 then
          c = cf
        else
          c = cn
        end
        ltex:set(x, y, c)
      end
    end
    rib_tex_levels[lvl] = ltex
  end
end

local function rib_precompute_basis()
  for i=0,RIB.subdiv do
    local u  = i / RIB.subdiv
    local u2 = u*u
    local u3 = u2*u
    rib_bw0[i] = -0.5*u3 + 1.0*u2 - 0.5*u
    rib_bw1[i] =  1.5*u3 - 2.5*u2 + 1.0
    rib_bw2[i] = -1.5*u3 + 2.0*u2 + 0.5*u
    rib_bw3[i] =  0.5*u3 - 0.5*u2
  end
end

local function rib_precompute_z()
  local knot_z = rib_knot_z
  local dz_knot = (RIB.zfar - RIB.znear) / (RIB.knots - 1)
  for k=1,RIB.knots do
    knot_z[k] = RIB.znear + (k - 1) * dz_knot
  end
  local n = 0
  for seg=1,RIB.knots-3 do
    local z0,z1,z2,z3 = knot_z[seg], knot_z[seg+1], knot_z[seg+2], knot_z[seg+3]
    local imax = RIB.subdiv
    if seg < RIB.knots - 3 then imax = RIB.subdiv - 1 end
    for i=0,imax do
      n = n + 1
      local z = z0*rib_bw0[i] + z1*rib_bw1[i] + z2*rib_bw2[i] + z3*rib_bw3[i]
      rib_FZ[n] = z
      rib_FSCALE[n] = z > RIB_CLIP_Z and (rib_dyn_focal / z) or 0
    end
  end
end

local function rib_compute_paths(tt)
  local nknots  = RIB.knots
  local nribbons = RIB.ribbons
  local subdiv  = RIB.subdiv
  local znear   = RIB.znear
  local flow    = RIB.flow
  local pax, pbx = RIB.path_ax, RIB.path_bx
  local pay, pby = RIB.path_ay, RIB.path_by
  local dz_knot = (RIB.zfar - znear) / (nknots - 1)
  local camx = rib_dyn_cam_x
  local camy = rib_dyn_cam_y
  local spread = rib_dyn_spread
  local helix_r = rib_dyn_helix_r
  local wob = rib_dyn_wob
  local twist = rib_dyn_twist
  local spin = rib_dyn_spin
  local fx, fy = rib_fx, rib_fy
  local fz_off = rib_fz_off
  local bw0, bw1, bw2, bw3 = rib_bw0, rib_bw1, rib_bw2, rib_bw3
  local idx = 0
  local nknots_m3 = nknots - 3
  local wob_norm = wob / 16.6  -- normalize so path center scales with wobble (0→straight)

  for rid=1,nribbons do
    local phase = (rid - 1) * TAU * spread / nribbons
    local kx, ky, kz_off = rib_kx, rib_ky, rib_kz_off_k

    for k=1,nknots do
      local z  = znear + (k - 1) * dz_knot
      local zs = z + tt * flow
      local cx_k = (sin(zs*0.030)*pax + sin(zs*0.011 - tt*0.18)*pbx) * wob_norm
      local cy_k = (cos(zs*0.026)*pay + sin(zs*0.010 + tt*0.15)*pby) * wob_norm
      local ang = zs*twist - tt*spin + phase
      local rr = helix_r + sin(zs*0.022 - tt*0.40 + rid*1.1)*wob
      kx[k] = cx_k + cos(ang)*rr       - camx
      ky[k] = cy_k + sin(ang)*rr*0.72  - camy
      kz_off[k] = sin(ang) * rr * 0.5
    end

    for seg=1,nknots_m3 do
      local p0x,p0y = kx[seg],  ky[seg]
      local p1x,p1y = kx[seg+1],ky[seg+1]
      local p2x,p2y = kx[seg+2],ky[seg+2]
      local p3x,p3y = kx[seg+3],ky[seg+3]
      local z0o,z1o,z2o,z3o = kz_off[seg],kz_off[seg+1],kz_off[seg+2],kz_off[seg+3]
      local imax = subdiv
      if seg < nknots_m3 then imax = subdiv - 1 end
      for i=0,imax do
        local a,b,c,d = bw0[i],bw1[i],bw2[i],bw3[i]
        idx = idx + 1
        fx[idx] = p0x*a + p1x*b + p2x*c + p3x*d
        fy[idx] = p0y*a + p1y*b + p2y*c + p3y*d
        fz_off[idx] = z0o*a + z1o*b + z2o*c + z3o*d
      end
    end
  end
end

local function rib_draw_quad(tex, vx, vy, vu, vv)
  local ix = rdq_ix
  ix[1] = 1; ix[2] = 2; ix[3] = 3; ix[4] = 4
  for i=2,4 do
    local key = ix[i]
    local ky  = vy[key]
    local j = i - 1
    while j >= 1 and vy[ix[j]] > ky do
      ix[j+1] = ix[j]
      j = j - 1
    end
    ix[j+1] = key
  end

  local i0,i1,i2,i3 = ix[1],ix[2],ix[3],ix[4]
  local y0,y1,y2,y3 = vy[i0],vy[i1],vy[i2],vy[i3]

  if y3 <= 0 or y0 >= H then return end

  -- precompute per-edge: ymin, ymax, and base+step for x/u/v
  -- so inner loop is just: val = base + sy * step
  local eymin, eymax = rdq_eymin, rdq_eymax
  local xbase, xstep = rdq_xbase, rdq_xstep
  local ubase, ustep = rdq_ubase, rdq_ustep
  local vbase, vstep = rdq_vbase, rdq_vstep

  -- edges: 1→2, 2→3, 3→4, 4→1
  for e=1,4 do
    local e1 = e + 1; if e1 > 4 then e1 = 1 end
    local ya, yb = vy[e], vy[e1]
    local dy = yb - ya
    if dy > 0.001 or dy < -0.001 then
      local inv = 1.0 / dy
      local xa, xb = vx[e], vx[e1]
      local ua, ub = vu[e], vu[e1]
      local va, vb = vv[e], vv[e1]
      local xs = (xb - xa) * inv
      local us = (ub - ua) * inv
      local vs = (vb - va) * inv
      xbase[e] = xa - ya * xs; xstep[e] = xs
      ubase[e] = ua - ya * us; ustep[e] = us
      vbase[e] = va - ya * vs; vstep[e] = vs
      if ya < yb then eymin[e] = ya; eymax[e] = yb
      else eymin[e] = yb; eymax[e] = ya end
    else
      eymin[e] = 1e9; eymax[e] = -1e9
    end
  end

  local sy = ceil(y0)
  if sy < 0 then sy = 0 end
  local ey_end = y3
  if ey_end > H then ey_end = H end

  while sy < ey_end do
    local lx, lu, lv =  1e9, 0, 0
    local rx, ru, rv = -1e9, 0, 0

    for e=1,4 do
      if sy >= eymin[e] and sy < eymax[e] then
        local ix_e = xbase[e] + sy * xstep[e]
        local iu   = ubase[e] + sy * ustep[e]
        local iv   = vbase[e] + sy * vstep[e]
        if ix_e < lx then lx, lu, lv = ix_e, iu, iv end
        if ix_e > rx then rx, ru, rv = ix_e, iu, iv end
      end
    end

    if rx > lx then
      _tline3d(tex, lx, sy, rx, sy, lu, lv, ru, rv, 1, 1, 0x300)
    end

    sy = sy + 1
  end
end

local function rib_init()
  rib_precompute_basis()
  rib_precompute_z()
  rib_build_texture()
  for i=1,RIB_TOTAL_PTS do rib_psx[i]=0; rib_psy[i]=0; rib_psc[i]=0 end
  for i=1,RIB_MAX_QUADS do rib_quad_zmid[i]=0; rib_quad_order[i]=i end
end


-----------------------------
-- TEXT HELPERS
-----------------------------

local function alpha_to_core_col(a)
  if a <= 0.02 then return nil end
  local t = (a - 0.02) / 0.98 * 5
  local idx = flr(t)
  if idx >= 5 then return 53 end
  if idx <= 0 then return 49 end
  return 48 + idx + 1
end

local function draw_masked(linebmp, x, y, col, w, h)
  if not col then return end
  pal(1, col, 0)
  sspr(linebmp, 0, 0, w, h, x, y, w*TXT_SCALE, h*TXT_SCALE)
end

local function draw_blur_line(item, cx, cy, amount, blur_px, xbias)
  local core = alpha_to_core_col(amount)
  if not core then return end

  local w, h = item.w, item.h
  local dw, dh = w * TXT_SCALE, h * TXT_SCALE
  local x = flr(cx - dw * 0.5 + xbias)
  local y = flr(cy - dh * 0.5)

  palt(0, true)

  if blur_px >= 5 then
    local c = max(48, core - 3)
    draw_masked(item.bmp, x-5, y,   c, w, h)
    draw_masked(item.bmp, x+5, y,   c, w, h)
    draw_masked(item.bmp, x,   y-5, c, w, h)
    draw_masked(item.bmp, x,   y+5, c, w, h)
    draw_masked(item.bmp, x-4, y-3, c, w, h)
    draw_masked(item.bmp, x+4, y-3, c, w, h)
    draw_masked(item.bmp, x-4, y+3, c, w, h)
    draw_masked(item.bmp, x+4, y+3, c, w, h)
  end

  if blur_px >= 4 then
    local c = max(48, core - 3)
    draw_masked(item.bmp, x-4, y,   c, w, h)
    draw_masked(item.bmp, x+4, y,   c, w, h)
    draw_masked(item.bmp, x,   y-4, c, w, h)
    draw_masked(item.bmp, x,   y+4, c, w, h)
  end

  if blur_px >= 3 then
    local c = max(48, core - 2)
    draw_masked(item.bmp, x-3, y,   c, w, h)
    draw_masked(item.bmp, x+3, y,   c, w, h)
    draw_masked(item.bmp, x,   y-3, c, w, h)
    draw_masked(item.bmp, x,   y+3, c, w, h)
    draw_masked(item.bmp, x-2, y-2, c, w, h)
    draw_masked(item.bmp, x+2, y-2, c, w, h)
    draw_masked(item.bmp, x-2, y+2, c, w, h)
    draw_masked(item.bmp, x+2, y+2, c, w, h)
  end

  if blur_px >= 2 then
    local c = max(48, core - 1)
    draw_masked(item.bmp, x-2, y,   c, w, h)
    draw_masked(item.bmp, x+2, y,   c, w, h)
    draw_masked(item.bmp, x,   y-2, c, w, h)
    draw_masked(item.bmp, x,   y+2, c, w, h)
    draw_masked(item.bmp, x-1, y-1, c, w, h)
    draw_masked(item.bmp, x+1, y-1, c, w, h)
    draw_masked(item.bmp, x-1, y+1, c, w, h)
    draw_masked(item.bmp, x+1, y+1, c, w, h)
  end

  if blur_px > 1 then
    local c = core
    draw_masked(item.bmp, x-1, y,   c, w, h)
    draw_masked(item.bmp, x+1, y,   c, w, h)
    draw_masked(item.bmp, x,   y-1, c, w, h)
    draw_masked(item.bmp, x,   y+1, c, w, h)
  end

  draw_masked(item.bmp, x, y, core, w, h)
end

local function setup_additive_blend()
  poke(0x5508, 0x3f, 0x3f, 0x3f, 0x3f)

  local bright = {}
  for i=0,63 do bright[i] = 0 end
  for i=1,31 do bright[i] = i / 31 end
  for i=0,15 do bright[32+i] = (i / 15) end
  bright[48] = 0.10
  bright[49] = 0.25
  bright[50] = 0.40
  bright[51] = 0.60
  bright[52] = 0.80
  bright[53] = 1.00

  -- for a given brightness, find the best ribbon-range color (1-31)
  local function best_ribbon(b)
    local ri = flr(b * 31 + 0.5)
    if ri < 1 then ri = 1 end
    if ri > 31 then ri = 31 end
    return ri
  end

  -- for a given brightness, find the best text-range color (48-53)
  local function best_text(b)
    if b > 0.90 then return 53 end
    if b > 0.72 then return 52 end
    if b > 0.50 then return 51 end
    if b > 0.32 then return 50 end
    if b > 0.15 then return 49 end
    return 48
  end

  for new=0,63 do
    for old=0,63 do
      local result
      if new == 0 then
        -- transparent: keep old
        result = old
      else
        local bn = bright[new]
        local bo = bright[old]
        -- lighten: pick whichever is brighter
        local bmax = bn > bo and bn or bo
        -- stay in the text range if either pixel is text-range
        local new_is_text = (new >= 48 and new <= 53)
        local old_is_text = (old >= 48 and old <= 53)
        if new_is_text or old_is_text then
          result = best_text(bmax)
        else
          result = best_ribbon(bmax)
        end
      end
      poke(0x8000 + new * 64 + old, result)
    end
  end
end


------------------------------------------------------------
-- rocket parameters (active values, set via setparams)
--
-- params[2] = time         (animation time, drives everything)*
-- params[3] = text_line    (0=no text, 1=line1 sharp, 1.5=line1 half-blur, 2=line2 sharp, etc.)*
-- params[4] = bg_rot       (background rotation speed, 0.3 = default)*
-- params[5] = palette_id   (which palette to use, 0 = default)*
-- params[6] = helix_r      (helix radius, 21.0 = default)*
-- params[7] = spread       (ribbon angular spread, 3.0 = default)*
-- params[8] = ribbon_w     (ribbon thickness, 4.55 = default)*
-- params[9] = focal        (camera focal length, 120 = default)*
-- params[10]= ribbon_speed (ribbon animation speed, 1.0 = default)*
-- params[11]= spin         (rotation speed, 0.35 = default, negative reverses)*
-- params[12]= twist        (ribbon twist amount, 0.08 = default, scaled by 0.1 internally)*
-- params[13]= wobble       (helix wobble amplitude, 16.6 = default)
-- params[14]= flash_count  (max simultaneous flashing ribbons, 1 = default)*
-- params[15]= flash_rate   (flash occurrence, 0=none, 1=lots, 0.5 = default)*
-- params[16]= cam_x        (ribbon camera x offset, 0 = default)*
-- params[17]= cam_y        (ribbon camera y offset, 0 = default)*
-- params[18]= fade         (0=black, 1=normal, 2=white)*
-- params[19]= shake        (screen shake amplitude, 0=off)*
-- params[20]= glitch       (glitch intensity, 0=off)*
------------------------------------------------------------

greets = {
  -- effect parameters
  t            = 0,
  text_line    = 0,
  bg_rot       = 0.3,
  palette_id   = 0,
  helix_r      = 21.0,
  spread       = 3.0,
  ribbon_w     = 4.55,
  focal        = 120,
  ribbon_speed = 1.0,
  spin         = 0.35,
  twist        = 0.08,
  wobble       = 16.6,
  flash_count  = 1,
  flash_rate   = 0.5,
  cam_x        = 0,
  cam_y        = 0,
  fade         = 1.0,
  shake        = 0,
  glitch       = 0,

  -- internal data (created in load/init)
  packed_bg    = nil,
  packed_text  = nil,
  packed_pals  = {},
  bg_img       = nil,
  bg_tex       = nil,
  bg_tw        = 0,
  bg_th        = 0,
  lines        = {},
  line_count   = 0,
  cur_pal_id   = -1,
  _pal_dirty   = false,
  palette_entries = {},
}


greets.load = function(this)
--  this.packed_bg   = fetch("greets_background.pod")
  this.packed_text = fetch("greets_text.pod")
  this.packed_pals[0] = fetch("greets_pal_0.pod.pal")
  this.packed_pals[1] = fetch("greets_pal_1.pod.pal")

  local bg_raw = unpod(fetch("greets_background.pod"))
  if bg_raw then
    this.bg_img = bg_raw
    local iw, ih = bg_raw:width(), bg_raw:height()
    local pw, ph = 1, 1
    while pw < iw do pw *= 2 end
    while ph < ih do ph *= 2 end
    this.bg_tex = userdata("u8", pw, ph)
    set_draw_target(this.bg_tex)
    cls(0)
    palt(0, false)
    sspr(bg_raw, 0, 0, iw, ih, 0, 0, iw, ih)
    set_draw_target()
    this.bg_tw, this.bg_th = pw, ph
  end

    -- unpack text spritesheet: text uses palette indices 48-53, 0 = transparent
  local txt_raw = unpod(this.packed_text)
  if txt_raw then
    local tw = txt_raw:width()
    local th = txt_raw:height()
    local lh = LINE_H

    -- render pod into a clean u8 buffer via set_draw_target
    -- reset ALL palette/transparency state to ensure a clean 1:1 copy
    local txt_buf = userdata("u8", tw, th)
    set_draw_target(txt_buf)
    for i=0,63 do pal(i, i, 0) end
    for i=0,63 do palt(i, false) end
    cls(0)
    sspr(txt_raw, 0, 0, tw, th, 0, 0, tw, th)
    set_draw_target()

    -- detect text pixels: anything with index 48-53 is a text pixel
    local function is_text(x, y)
      local v = txt_buf:get(x, y)
      return v >= 48 and v <= 53
    end

    -- extract per-line monochrome bitmaps (0=bg, 1=text)
    this.line_count = flr(th / lh)
    this.lines = {}
    for li=0, this.line_count-1 do
      local y0 = li * lh
      local minx = tw
      local maxx = -1
      for y=0, lh-1 do
        for x=0, tw-1 do
          if is_text(x, y0 + y) then
            if x < minx then minx = x end
            if x > maxx then maxx = x end
          end
        end
      end
      if maxx < minx then minx = 0; maxx = 0 end
      local lw = maxx - minx + 1
      local bmp = userdata("u8", lw, lh)
      for y=0, lh-1 do
        for x=minx, maxx do
          if is_text(x, y0 + y) then
            bmp:set(x - minx, y, 1)
          end
        end
      end
      add(this.lines, { bmp = bmp, w = lw, h = lh })
    end
    this.line_count = #this.lines
  end

  -- init ribbons & additive blend LUT
  rib_init()
  setup_additive_blend()

  for i = 0, 63 do palt(i, false) end
  fillp()
end



greets.init = function(this)
  -- parse palettes from loaded .pal data
  for id, raw in pairs(this.packed_pals) do
    local entries = {}
    local text = raw
    if type(raw) == "table" then text = table.concat(raw, "\n") end
    if type(text) == "string" then
      for token in text:gmatch("[#]?([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])") do
        local v = tonumber(token, 16)
        if v then entries[#entries+1] = v end
      end
    end
    this.palette_entries[id] = entries
  end

  -- apply initial palette
  this:apply_palette(0)
end


greets.apply_palette = function(this, pal_id)
  local entries = this.palette_entries[pal_id]
  if not entries then entries = this.palette_entries[0] end
  if not entries then return end
  for i=0, min(#entries, 64)-1 do
    pal(i, entries[i+1], 2)
  end
  for i=0,31 do
    bg_base_pal[i] = entries[i+1] or 0
  end
  this.cur_pal_id = pal_id
end


greets.setparams = function(this, params)
  this.t            = params[2] or 0
  this.text_line    = params[3] or 0
  this.bg_rot       = params[4] or 0.3
  this.palette_id   = flr(params[5] or 0)
  this.helix_r      = params[6] or 21.0
  this.spread       = params[7] or 3.0
  -- nil check because 0 is falsy in picotron
  if params[8]  == nil then this.ribbon_w    = 4.55 else this.ribbon_w   = params[8]  end
  this.focal        = params[9] or 120
  this.ribbon_speed = params[10] or 1.0
  if params[11] == nil then this.spin        = 0.35 else this.spin       = params[11] end
  if params[12] == nil then this.twist       = 0.08  else this.twist     = params[12] end
  this.wobble       = params[13] or 16.6
  if params[14] == nil then this.flash_count = 1   else this.flash_count = params[14] end
  if params[15] == nil then this.flash_rate  = 0.5 else this.flash_rate  = params[15] end
  this.cam_x        = params[16] or 0
  this.cam_y        = params[17] or 0
  this.fade         = params[18] or 1.0
  this.shake        = params[19] or 0
  this.glitch       = params[20] or 0
end


greets.update = function(this)
  -- switch palette if needed
  if this.palette_id ~= this.cur_pal_id then
    this:apply_palette(this.palette_id)
  end

  -- apply fade to palette (0=black, 1=normal, 2=white)
  local entries = this.palette_entries[this.cur_pal_id]
  if not entries then entries = this.palette_entries[0] end
  if entries then
    local fade = this.fade
    if fade ~= 1.0 then
      for i = 0, 63 do
        local c
        if i < #entries then
          c = entries[i+1]
        else
          c = peek4(0x5000 + i * 4) & 0xffffff
        end
        local r = (c >> 16) & 0xff
        local g = (c >> 8) & 0xff
        local b = c & 0xff
        if fade < 1.0 then
          r = r * fade; g = g * fade; b = b * fade
        else
          local ft = fade - 1.0
          r = r + (255 - r) * ft; g = g + (255 - g) * ft; b = b + (255 - b) * ft
        end
        pal(i, (flr(r) << 16) | (flr(g) << 8) | flr(b), 2)
      end
      this._pal_dirty = true
    elseif this._pal_dirty then
      for i = 0, min(#entries, 64)-1 do
        pal(i, entries[i+1], 2)
      end
      -- reset colors beyond palette entries to defaults
      for i = #entries, 63 do pal(i, i, 2) end
      this._pal_dirty = false
    end
    -- store unfaded palette for bg flash index-shifting
    for i=0,31 do
      bg_base_pal[i] = entries[i+1] or 0
    end
  end
end


greets.draw = function(this)
  local t = this.t

  -- background flash
  if t >= bg_flash_next and bg_flash_start < 0 then
    bg_flash_start = t
    bg_flash_dur = BG_FLASH_MIN_DUR + rnd(1) * (BG_FLASH_MAX_DUR - BG_FLASH_MIN_DUR)
  end
  if bg_flash_start >= 0 then
    local p = (t - bg_flash_start) / bg_flash_dur
    local fade = this.fade
    if p >= 1 then
      bg_flash_start = -99
      bg_flash_next = t + BG_FLASH_MIN_GAP + rnd(1) * (BG_FLASH_MAX_GAP - BG_FLASH_MIN_GAP)
      for i=0,31 do
        local c = bg_base_pal[i]
        if fade ~= 1.0 then c = fade_color(c, fade) end
        pal(i, c, 2)
      end
    else
      local bright = p < 0.25 and smoothstep01(p / 0.25) or smoothstep01(1 - (p - 0.25) / 0.75)
      local shift = flr(bright * BG_FLASH_SHIFT + 0.5)
      for i=0,31 do
        local src = i + shift
        if src > 31 then src = 31 end
        local c = bg_base_pal[src]
        if fade ~= 1.0 then c = fade_color(c, fade) end
        pal(i, c, 2)
      end
    end
  end

  cls(0)

  -- shake: offset drawing origin
  local shake_ox, shake_oy = 0, 0
  if this.shake > 0 then
    local a = this.shake
    shake_ox = flr((irnd(2000) / 1000 - 1) * a)
    shake_oy = flr((irnd(2000) / 1000 - 1) * a)
    camera(-shake_ox, -shake_oy)
  end

  -- draw rotating background image
  if this.bg_tex then
    palt(0, false)
    poke2(0x5530, 0)
    poke2(0x5532, 0)
    poke2(0x5534, this.bg_tw)
    poke2(0x5536, this.bg_th)

    local rot = t * this.bg_rot
    local ca, sa = cos(rot), -sin(rot)
    local ca_abs = abs(ca)
    local sa_abs = abs(sa)
    local ihw, ihh = this.bg_img:width() * 0.5, this.bg_img:height() * 0.5
    local bw = (W * 0.5) * ca_abs + (H * 0.5) * sa_abs
    local bh = (W * 0.5) * sa_abs + (H * 0.5) * ca_abs
    local zoom = max(1.0, max(bw / ihw, bh / ihh))
    local inv_z = 1.0 / zoom
    local cx_bg = ihw
    local cy_bg = ihh

    -- precompute scanline-constant terms; increment by delta per row
    local sx_l = (-W * 0.5) * inv_z
    local sx_r = ( W * 0.5) * inv_z
    local lu_base = cx_bg + sx_l * ca
    local lv_base = cy_bg + sx_l * sa
    local ru_base = cx_bg + sx_r * ca
    local rv_base = cy_bg + sx_r * sa
    local sy0 = (-H * 0.5) * inv_z
    local lu = lu_base - sy0 * sa
    local lv = lv_base + sy0 * ca
    local ru = ru_base - sy0 * sa
    local rv = rv_base + sy0 * ca
    local du = -sa * inv_z   -- per-scanline u delta
    local dv =  ca * inv_z   -- per-scanline v delta
    for y = 0, H - 1 do
      _tline3d(this.bg_tex, 0, y, W - 1, y, lu, lv, ru, rv, 1, 1, 0)
      lu = lu + du
      lv = lv + dv
      ru = ru + du
      rv = rv + dv
    end

    poke2(0x5530, 0)
    poke2(0x5532, 0)
    poke2(0x5534, RIB_TEXW)
    poke2(0x5536, RIB_TEXH)
  end

  -- ribbons
  -- update dynamic ribbon params from rocket
  rib_dyn_spread  = this.spread
  rib_dyn_helix_r = this.helix_r
  rib_dyn_wob     = this.wobble
  rib_dyn_focal   = this.focal
  rib_dyn_spin    = this.spin
  rib_dyn_twist   = this.twist * 0.1
  rib_dyn_cam_x   = this.cam_x
  rib_dyn_cam_y   = this.cam_y

  -- recompute z-projection only when focal length changes
  if rib_dyn_focal ~= rib_last_focal then
    rib_precompute_z()
    rib_last_focal = rib_dyn_focal
  end

  RIB_W_VAL = this.ribbon_w
  rib_time_acc = t * this.ribbon_speed

  rib_compute_paths(rib_time_acc)
  local fx = rib_fx
  local fy = rib_fy

  for rid=1,RIB.ribbons do
    local base = (rid - 1) * RIB_POINTS_PER
    for li=1,RIB_POINTS_PER do
      local i = base + li
      local s = rib_FSCALE[li]
      if s > 0 then
        rib_psx[i] = CX + fx[i] * s
        rib_psy[i] = CY + fy[i] * s
        rib_psc[i] = s
      else
        rib_psc[i] = 0
      end
    end
  end

  -- build quads
  local qc = 0
  local W80, H80 = W + 80, H + 80
  local inv_zrange = 1.0 / (RIB_ZFAR - RIB_CLIP_Z)
  local U_LEFT  = 0.5
  local U_RIGHT = RIB_TEXW - 0.5
  local V_CLOSE = RIB_TEXH - 0.5
  local V_RANGE = RIB_TEXH - 1.0

  -- compute per-ribbon average z offset for layer ordering
  local rib_layer_z = rib_layer_z_arr
  for rid=1,RIB.ribbons do
    local base = (rid - 1) * RIB_POINTS_PER
    local sum = 0
    for li=1,RIB_POINTS_PER do
      sum = sum + rib_fz_off[base + li]
    end
    rib_layer_z[rid] = sum / RIB_POINTS_PER
  end
  -- sort ribbon IDs by layer z (back-to-front: largest z first)
  local rib_draw_order = rib_draw_order_arr
  for rid=1,RIB.ribbons do rib_draw_order[rid] = rid end
  for i=2,RIB.ribbons do
    local key = rib_draw_order[i]
    local kz  = rib_layer_z[key]
    local j = i - 1
    while j >= 1 and rib_layer_z[rib_draw_order[j]] < kz do
      rib_draw_order[j+1] = rib_draw_order[j]
      j = j - 1
    end
    rib_draw_order[j+1] = key
  end

  -- build quads in ribbon-layer order (back ribbon first, front ribbon last)
  for ro=1,RIB.ribbons do
    local rid = rib_draw_order[ro]
    local base = (rid - 1) * RIB_POINTS_PER

    for li=1,RIB_POINTS_PER do
      local i = base + li
      local s = rib_psc[i]
      if s <= 0 then
        rib_ptxl[li] = nil
      else
        local sx_i = rib_psx[i]
        local sy_i = rib_psy[i]
        local anx, any = 0, 0
        local got = false

        if li > 1 and rib_psc[base + li - 1] > 0 then
          local dx = sx_i - rib_psx[base + li - 1]
          local dy = sy_i - rib_psy[base + li - 1]
          local ll = dx*dx + dy*dy
          if ll > 0.01 then
            local inv = 1.0 / sqrt(ll)
            anx = anx - dy * inv
            any = any + dx * inv
            got = true
          end
        end

        if li < RIB_POINTS_PER and rib_psc[base + li + 1] > 0 then
          local dx = rib_psx[base + li + 1] - sx_i
          local dy = rib_psy[base + li + 1] - sy_i
          local ll = dx*dx + dy*dy
          if ll > 0.01 then
            local inv = 1.0 / sqrt(ll)
            anx = anx - dy * inv
            any = any + dx * inv
            got = true
          end
        end

        if got then
          local nl = anx*anx + any*any
          if nl > 0.0001 then
            local inv = 1.0 / sqrt(nl)
            anx = anx * inv
            any = any * inv
          end
          local hw = RIB_W_VAL * s; if hw > RIB_MAX_HW then hw = RIB_MAX_HW end
          local ts = RIB.taper_segs
          if li <= ts then
            hw = hw * (li - 1) / ts
          elseif li > RIB_POINTS_PER - ts then
            hw = hw * (RIB_POINTS_PER - li) / ts
          end
          rib_ptxl[li] = sx_i - anx*hw
          rib_ptyl[li] = sy_i - any*hw
          rib_ptxr[li] = sx_i + anx*hw
          rib_ptyr[li] = sy_i + any*hw
        else
          rib_ptxl[li] = nil
        end
      end
    end

    for seg=1,RIB_SEGMENTS do
      local li0 = seg
      local li1 = seg + 1
      local z0 = rib_FZ[li0]
      local z1 = rib_FZ[li1]

      if (z0 > RIB_CLIP_Z or z1 > RIB_CLIP_Z) and rib_ptxl[li0] and rib_ptxl[li1] then
        local x0l, y0l = rib_ptxl[li0], rib_ptyl[li0]
        local x0r, y0r = rib_ptxr[li0], rib_ptyr[li0]
        local x1l, y1l = rib_ptxl[li1], rib_ptyl[li1]
        local x1r, y1r = rib_ptxr[li1], rib_ptyr[li1]

        local minx = x0l; if x0r < minx then minx = x0r end
        if x1l < minx then minx = x1l end; if x1r < minx then minx = x1r end
        local maxx = x0l; if x0r > maxx then maxx = x0r end
        if x1l > maxx then maxx = x1l end; if x1r > maxx then maxx = x1r end
        local miny = y0l; if y0r < miny then miny = y0r end
        if y1l < miny then miny = y1l end; if y1r < miny then miny = y1r end
        local maxy = y0l; if y0r > maxy then maxy = y0r end
        if y1l > maxy then maxy = y1l end; if y1r > maxy then maxy = y1r end

        if maxx > -80 and minx < W80 and maxy > -80 and miny < H80 then
          local cz0 = z0; if cz0 < RIB_CLIP_Z then cz0 = RIB_CLIP_Z end
          local cz1 = z1; if cz1 < RIB_CLIP_Z then cz1 = RIB_CLIP_Z end
          local t0 = (cz0 - RIB_CLIP_Z) * inv_zrange
          local t1 = (cz1 - RIB_CLIP_Z) * inv_zrange
          if t0 > 1 then t0 = 1 end
          if t1 > 1 then t1 = 1 end

          -- sort key: ribbon layer (back-to-front) as primary, z as secondary
          qc = qc + 1
          rib_quad_zmid[qc] = ro * 10000 + max(z0, z1)
          rib_quad_order[qc] = qc
          rib_q_rid[qc] = rid
          rib_q_x0l[qc] = x0l; rib_q_y0l[qc] = y0l
          rib_q_x0r[qc] = x0r; rib_q_y0r[qc] = y0r
          rib_q_x1l[qc] = x1l; rib_q_y1l[qc] = y1l
          rib_q_x1r[qc] = x1r; rib_q_y1r[qc] = y1r
          rib_q_v0[qc] = V_CLOSE - t0 * V_RANGE
          rib_q_v1[qc] = V_CLOSE - t1 * V_RANGE
        end
      end
    end
  end

  -- depth sort: ribbon-layer first (back-to-front), z within ribbon
  if qc > 1 then
    for i=2,qc do
      local key = rib_quad_order[i]
      local kz  = rib_quad_zmid[key]
      local j = i - 1
      while j >= 1 and rib_quad_zmid[rib_quad_order[j]] < kz do
        rib_quad_order[j+1] = rib_quad_order[j]
        j = j - 1
      end
      rib_quad_order[j+1] = key
    end
  end

  -- draw ribbon quads with per-ribbon flash
  palt(0, false)

  -- deterministic flash: divide time into periods, each period assigns ribbons
  -- flash_rate controls period length (0=off, 1=fast ~0.6s period)
  -- flash_count controls how many ribbons per period (1,2,3...)
  local rib_levels = rib_levels_arr
  for rid=1,RIB.ribbons do rib_levels[rid] = 0 end

  local fr = this.flash_rate
  local fc = this.flash_count
  if fr > 0 and fc > 0 then
    local period = RIB_FLASH_BASE_PERIOD / fr
    local dur = RIB_FLASH_DUR
    -- how many overlapping flash slots can be active
    local slots = max(1, ceil(dur / period))
    local nrib = RIB.ribbons
    local count = min(flr(fc), nrib)

    for s = 0, slots - 1 do
      -- which period index does this slot correspond to
      local pidx = flr(t / period) - s
      local flash_start = pidx * period
      local elapsed = t - flash_start
      if elapsed >= 0 and elapsed < dur then
        -- compute intensity envelope
        local phase = elapsed / dur
        local intensity
        if phase < 0.35 then
          intensity = smoothstep01(phase / 0.35)
        else
          intensity = smoothstep01((1.0 - phase) / 0.65)
        end
        local lvl = flr(intensity * RIB_FLASH_LEVELS + 0.5)
        if lvl > 0 then
          -- deterministic ribbon selection using period index as seed
          -- simple hash: spread ribbons across periods
          for c_i = 0, count - 1 do
            local rid = ((pidx * 7 + c_i * 3) % nrib) + 1
            if rib_levels[rid] < lvl then
              rib_levels[rid] = lvl
            end
          end
        end
      end
    end
  end

  local _vx, _vy, _vu, _vv = rib_qvx, rib_qvy, rib_qvu, rib_qvv
  for i=1,qc do
    local qi = rib_quad_order[i]
    local v0 = rib_q_v0[qi]
    local v1 = rib_q_v1[qi]
    _vx[1] = rib_q_x0l[qi]; _vy[1] = rib_q_y0l[qi]; _vu[1] = U_LEFT;  _vv[1] = v0
    _vx[2] = rib_q_x0r[qi]; _vy[2] = rib_q_y0r[qi]; _vu[2] = U_RIGHT; _vv[2] = v0
    _vx[3] = rib_q_x1r[qi]; _vy[3] = rib_q_y1r[qi]; _vu[3] = U_RIGHT; _vv[3] = v1
    _vx[4] = rib_q_x1l[qi]; _vy[4] = rib_q_y1l[qi]; _vu[4] = U_LEFT;  _vv[4] = v1
    local lvl = rib_levels[rib_q_rid[qi]]
    if lvl > 0 then
      rib_draw_quad(rib_tex_levels[lvl], _vx, _vy, _vu, _vv)
    else
      rib_draw_quad(rib_tex, _vx, _vy, _vu, _vv)
    end
  end

  -- draw greetings text overlay (reset draw palette first so pal remapping is clean)
  pal(0)
  local tl = this.text_line
  if tl > 0 and this.line_count > 0 then
    local line_idx = flr(tl)
    local frac = tl - line_idx

    -- wrap line index into valid range (1-based)
    line_idx = ((line_idx - 1) % this.line_count) + 1
    local item = this.lines[line_idx]

    if item then
      if frac < 0.01 then
        -- sharp display
        draw_blur_line(item, CX, CY, 1.0, 1, 0)
      else
        -- fractional part drives blur/fadeout: 0→sharp, 1→gone
        local tr = smoothstep01(frac)
        local a = 1.0 - tr
        local blur = 1 + flr(tr * 8)
        local shift = -10 * tr

        -- trailing ghost copies
        for i = 1, 3 do
          local tp = i / 3
          local ta = a * (1.0 - tp) * 0.4
          local ty = CY - (tp * 15)
          if ta > 0.01 then
            draw_blur_line(item, CX + shift * tp, ty, ta, max(1, blur - 2), 0)
          end
        end
        draw_blur_line(item, CX, CY, a, blur, shift)
      end
    end
  end

  palt()
  pal(0)

  -- reset camera offset
  if shake_ox ~= 0 or shake_oy ~= 0 then
    camera(0, 0)
  end

  -- glitch post-processing: horizontal slice displacement
  if this.glitch > 0 then
    local disp = get_display()
    local amp = this.glitch * 18  -- scale: 1.0 = 18px max displacement
    local slices = 24
    local iw, ih = W, H
    local snap = userdata("u8", iw, ih)
    snap:copy(disp, true, 0, 0, iw * ih, 0, 0, 1)
    for s = 0, slices - 1 do
      local sy0 = flr(s * ih / slices)
      local sy1 = flr((s + 1) * ih / slices)
      local sh = sy1 - sy0
      if sh > 0 then
        local off = flr((irnd(2000) / 1000 - 1) * amp)
        if irnd(9) == 0 then off += (irnd(2) * 2 - 1) * flr(amp * 0.9) end
        if off != 0 then
          for y = sy0, sy1 - 1 do
            local row_off = y * iw
            if off > 0 then
              disp:copy(snap, true, row_off, row_off + off, iw - off, 0, 0, 1)
              disp:copy(0, true, 0, row_off, off, 0, 0, 1)
            else
              local a = -off
              disp:copy(snap, true, row_off + a, row_off, iw - a, 0, 0, 1)
              disp:copy(0, true, 0, row_off + iw - a, a, 0, 0, 1)
            end
          end
        end
      end
    end
  end
end
