local sin, cos, abs = sin, cos, abs
local tline3d = tline3d

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

-- file-scope state for uv_at (avoids passing blob positions as args each call)
local _b1x, _b1y, _b2x, _b2y = 0, 0, 0, 0
local _thw, _thh = 0, 0

local function fix_wrap(l, r, span)
  local d = r - l
  local h = span * 0.5
  if d > h then
    r -= span
  elseif d < -h then
    r += span
  end
  return r
end

local function uv_at(x, y, ca, sa, inv_scale, ws, wr2_l, ou, ov)
  local sx = (x - 119.5) * inv_scale
  local sy = (y - 67) * inv_scale

  local u = sx * ca - sy * sa
  local v = sx * sa + sy * ca

  local ox, oy = 0, 0

  local dx = x - _b1x
  local dy = y - _b1y
  local d2 = dx * dx + dy * dy
  if d2 < wr2_l then
    local k = (wr2_l - d2) / wr2_l
    ox -= dx * k
    oy -= dy * k
  end

  dx = x - _b2x
  dy = y - _b2y
  d2 = dx * dx + dy * dy
  if d2 < wr2_l then
    local k = (wr2_l - d2) / wr2_l
    ox -= dx * k
    oy -= dy * k
  end

  u += ox * ws
  v += oy * ws

  return _thw + u + ou, _thh + v + ov
end

------------------------------------------------------------
-- rocket parameters (active values, set via setparams)
--
-- params[2] = rotation    (degrees, rotation angle)
-- params[3] = scale       (texture scale, e.g. 3.0)
-- params[4] = zoom        (zoom multiplier, 1.0 = neutral)
-- params[5] = scroll_u    (UV scroll offset U)
-- params[6] = scroll_v    (UV scroll offset V)
-- params[7] = warp        (distortion strength, 0..2)
-- params[8] = blob_phase  (time param for blob orbits)
-- params[9] = fade        (brightness 0..1)
-- params[10]= offset_u    (texture center shift U, pixels)
-- params[11]= offset_v    (texture center shift V, pixels)
-- params[12]= shake       (screen shake amplitude, 0=off)
-- params[13]= glitch      (glitch intensity, 0=off)
------------------------------------------------------------

roto_distort = {
  rotation   = 0,
  scale      = 3.0,
  zoom       = 1.0,
  scroll_u   = 0,
  scroll_v   = 0,
  warp       = 0.55,
  blob_phase = 0,
  fade       = 1.0,
  offset_u   = 0,
  offset_v   = 0,
  shake      = 0,
  glitch     = 0,

  -- internal
  packed_tex = nil,
  tex = nil,
  tw = 0,
  th = 0,
  wr2 = 62 * 62,
  cell = 8,

  palette = { [0]=
    0x000000, 0x08040a, 0x080808, 0x0f0d0e, 0x171115, 0x1e131f, 0x1e1326, 0x17112e,
    0x0e1536, 0x10244a, 0x11365e, 0x104e72, 0x266587, 0x427f9d, 0x659ab3, 0x8eb7c9,
    0xbdd5df, 0x352910, 0x4e381b, 0x684729, 0x82543a, 0x9b614c, 0xb56f61, 0xcf7e78,
    0x8d746e, 0xb99b95, 0xcbb0a6, 0xe5c3bd, 0xede2dc, 0x180808, 0x2c0d0f, 0x401016,
    0x55111f, 0x691128, 0x7e0f34, 0x45233c, 0x732c66, 0xa12993, 0xcf1bc6, 0xfd00fd,
    0xf067c9, 0xff00ff, 0x130d20, 0x141d28, 0x1c302d, 0x253828, 0x2c4640, 0x334854,
    0x181808, 0x282628, 0x2c2e29, 0x41454a, 0x565c6b, 0x000000, 0x000000, 0x000000,
    0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000
  }
}


roto_distort.load = function(this)
  this.tex = unpod(fetch("tex_eye.pod"))
  this.tw = this.tex:width()
  this.th = this.tex:height()
end


roto_distort.init = function(this)
  _thw = this.tw * 0.5
  _thh = this.th * 0.5

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


roto_distort.setparams = function(this, params)
  this.rotation   = params[2] or 0
  this.scale      = params[3] or 3.0
  this.zoom       = params[4] or 1.0
  this.scroll_u   = params[5] or 0
  this.scroll_v   = params[6] or 0
  this.warp       = params[7] or 0.55
  this.blob_phase = params[8] or 0
  this.fade       = params[9] or 1.0
  this.offset_u   = params[10] or 0
  this.offset_v   = params[11] or 0
  this.shake      = params[12] or 0
  this.glitch     = params[13] or 0
end


roto_distort.update = function(this)
  local colors = this.palette
  local fade = this.fade
  for i = 0, 63 do
    local c = colors[i]
    local r = (c >> 16) & 0xff
    local g = (c >> 8) & 0xff
    local b = c & 0xff
    if fade < 1.0 then
      r = flr(r * fade)
      g = flr(g * fade)
      b = flr(b * fade)
    elseif fade > 1.0 then
      local t = fade - 1.0
      r = flr(r + (255 - r) * t)
      g = flr(g + (255 - g) * t)
      b = flr(b + (255 - b) * t)
    end
    pal(i, (r << 16) | (g << 8) | b, 2)
  end
end


roto_distort.draw = function(this)
  local iw, ih = 240, 135
  local tex = this.tex
  local tw, th = this.tw, this.th

  -- enable tline3d texture wrapping
  poke2(0x5530, 0)
  poke2(0x5532, 0)
  poke2(0x5534, tw)
  poke2(0x5536, th)

  -- 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)
  end

  cls(0)

  -- rotation (degrees -> turns for sin/cos)
  local rot = this.rotation / 360.0
  local ca, sa = cos(rot), -sin(rot)

  -- scale & zoom
  local inv_scale = 1 / (this.scale * this.zoom)

  -- scroll offsets (includes texture center shift)
  local ou = this.scroll_u + this.offset_u
  local ov = this.scroll_v + this.offset_v

  -- warp
  local ws = this.warp
  local wr2_l = this.wr2

  -- compute blob positions from blob_phase
  local tt = this.blob_phase
  local r0 = 52 * abs(sin(tt * 0.6))

  local a1 = tt * 1.10 + 1 / 3
  local r1 = 10 + r0 * (0.55 + 0.25 * sin(tt * 0.8 + 1))
  _b1x = iw * 0.5 + cos(a1) * r1
  _b1y = ih * 0.5 + sin(a1 * 1.2) * r1

  local a2 = tt * 1.35 + 2 / 3
  local r2 = 10 + r0 * (0.55 + 0.25 * sin(tt * 0.8 + 2))
  _b2x = iw * 0.5 + cos(a2) * r2
  _b2y = ih * 0.5 + sin(a2 * 1.2) * r2

  local cell = this.cell

  for y0 = 0, ih - 1, cell do
    local bh = cell
    if y0 + bh > ih then bh = ih - y0 end

    local tl_u, tl_v = uv_at(0, y0, ca, sa, inv_scale, ws, wr2_l, ou, ov)
    local bl_u, bl_v = uv_at(0, y0 + bh, ca, sa, inv_scale, ws, wr2_l, ou, ov)

    for x0 = 0, iw - 1, cell do
      local bw = cell
      if x0 + bw > iw then bw = iw - x0 end
      local x1 = x0 + bw

      local tr_u, tr_v = uv_at(x1, y0, ca, sa, inv_scale, ws, wr2_l, ou, ov)
      local br_u, br_v = uv_at(x1, y0 + bh, ca, sa, inv_scale, ws, wr2_l, ou, ov)

      local lu, lv = tl_u, tl_v
      local ru, rv = tr_u, tr_v
      local ldu = (bl_u - tl_u) / bh
      local ldv = (bl_v - tl_v) / bh
      local rdu = (br_u - tr_u) / bh
      local rdv = (br_v - tr_v) / bh

      for i = 0, bh - 1 do
        local ru2 = fix_wrap(lu, ru, tw)
        local rv2 = fix_wrap(lv, rv, th)
        tline3d(tex, x0 + shake_ox, y0 + i + shake_oy, x1 + shake_ox, y0 + i + shake_oy, lu, lv, ru2, rv2, 1, 1, 0)
        lu += ldu  lv += ldv
        ru += rdu  rv += rdv
      end

      tl_u, tl_v = tr_u, tr_v
      bl_u, bl_v = br_u, br_v
    end
  end

  -- glitch post-processing
  if this.glitch > 0 then
    local disp = get_display()
    local amp = this.glitch * 18  -- scale: 1.0 = 18px max displacement
    local slices = 24
    -- snapshot the display so overlapping copies don't smear
    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
              -- shift right: copy from snap, fill left edge with black
              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
              -- shift left: copy from snap, fill right edge with black
              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
