import { getWarpBeatsFromSeconds } from '@suno/studiokit/warpUtils';
import { memoize } from 'lodash-es';
import * as THREE from 'three';

import { CanvasRegion } from '@/components/edit2025/canvasRenderer/CanvasRenderer';
import { lighten, shiftHue } from '@/utils/colorBlending';
import { dspEngineSingleton } from '@/utils/dsp';

import { StudioContextType } from '../StudioContext';
import { CLIP_CORNER_RADIUS } from '../constants';
import fInterpTo from '../fInterpTo';
import getCanvasRelativeRect from '../getCanvasRelativeRect';
import {
  getSongEndBeats,
  getSongStartBeats,
  getTakeLaneParentsByTrackId,
  getTracksAndExpandedTakeLanes,
} from '../selectors';
import { StudioClip } from '../types';
import { getAvailableDuration } from '../useFfmpegBufferCache';
import { ensureStreamingFfmpegBufferPrewarmed } from '../useFfmpegBufferCache';

const clipCornerRadius = CLIP_CORNER_RADIUS + 3;

const ENABLE_MOUSE = false;

/**********************
 * Tunables / Defaults
 **********************/
const CACHE_KEY = 'makeClipCreationIntentWebglRegions_Stripes_KawaseV1';

const getStripePalette = memoize((inputColor: string) => {
  return [
    {
      hex: shiftHue(inputColor.slice(0, 7), 0).slice(0, 7) as `#${string}`,
      weight: 1,
    },
    {
      hex: shiftHue(inputColor.slice(0, 7), -0.05).slice(0, 7) as `#${string}`,
      weight: 2,
    },
    {
      hex: shiftHue(inputColor.slice(0, 7), 0.05).slice(0, 7) as `#${string}`,
      weight: 2,
    },
    {
      hex: lighten(inputColor.slice(0, 7), 0.5).slice(0, 7) as `#${string}`,
      weight: 2,
    },
    {
      hex: lighten(inputColor.slice(0, 7), 0.7).slice(0, 7) as `#${string}`,
      weight: 2,
    },
  ] as Array<{ hex: `#${string}`; weight: number }>;
});

const STRIPES_MIX_INTENSITY = 0.9; // insert gradient stripes between neighbors
const BLUR_PX = 25; // perceived blur strength; mapped to Kawase levels
const GRAIN_STRENGTH = 0.03; // 0..1
const SPOTLIGHT = { enabled: false, size: 400, opacity: 0.7 }; // can leave disabled

// Expand GPU scissor a bit so AA/blur rims aren't clipped
const AA_EXPAND_DEVICE_PX = 2;

// Expand the virtual mask a hair & only fade very dark hues at the edge
const EDGE_PAD_PX = 1.5;
const DARK_EDGE_LUMA_MIN = 0.03; // Lower threshold - fade even darker colors
const DARK_EDGE_LUMA_MAX = 0.12; // Lower threshold - fade even darker colors

// Kawase mapping (tweak if you want creamier blur at same BLUR_PX)
const KAWASE_MAX_LEVELS = 8;
const kawaseLevelsFromBlur = (blurPx: number) =>
  Math.min(KAWASE_MAX_LEVELS, Math.max(1, Math.round(blurPx / 10)));

// Screen-space LOD for stripes
const MIN_KAWASE_RT_PX = 8; // don't downsample below this; preserves detail

// Tunables / Defaults
const SCROLL_SECONDS_PER_WIDTH = 16; // slower than 10s
const MUTATION_INTERVAL_MS = 400;
const BREATHING_INTERVAL_MS = 200;
const SOFT_EDGE_FRAC = 0.35; // 0..0.5 recommended for soft joins
// Stripe repeat period in **timeline beats**. This makes the pattern scale with zoom
// and stay anchored to the same beat positions as you zoom/pan.
const STRIPE_PERIOD_BEATS = 16;
const TARGET_STRIPE_PERIOD_DEVICE_PX = 180;
const MAX_STRIPE_PERIOD_MULTIPLIER = 24;

// Incommensurate noise scales (px) – pick numbers that don't share small factors
const NOISE_SCALE_1_PX = 260;
const NOISE_SCALE_2_PX = 997;
// Tiny per-cell color/weight jitter (0..1) – keep small to avoid banding
const COLOR_JITTER = 0.16;

/*********************
 * WebGL/Three plumbing
 *********************/
type ThreeRendererEntry = {
  renderer: THREE.WebGLRenderer;
  width: number;
  height: number;
};

const threeByGL = new WeakMap<WebGL2RenderingContext, ThreeRendererEntry>();
const prewarmedClipIds = new Set<string>();
function getOrCreateThree(gl: WebGL2RenderingContext) {
  const width = gl.drawingBufferWidth;
  const height = gl.drawingBufferHeight;
  let entry = threeByGL.get(gl);
  if (!entry) {
    const canvas = gl.canvas as HTMLCanvasElement;
    const renderer = new THREE.WebGLRenderer({
      canvas,
      context: gl,
      alpha: true,
      antialias: false,
      powerPreference: 'high-performance',
      depth: false,
      stencil: false,
      premultipliedAlpha: true,
      preserveDrawingBuffer: false,
    });
    renderer.setPixelRatio(1); // we pass device px explicitly
    renderer.setSize(width, height, false);
    renderer.outputColorSpace = THREE.SRGBColorSpace;
    renderer.autoClear = false;
    try {
      (renderer.getContext() as WebGL2RenderingContext).enable(
        gl.SAMPLE_ALPHA_TO_COVERAGE
      );
    } catch {}
    entry = { renderer, width, height };
    threeByGL.set(gl, entry);
  } else {
    if (entry.width !== width || entry.height !== height) {
      entry.renderer.setSize(width, height, false);
      entry.width = width;
      entry.height = height;
    }
  }
  return entry.renderer;
}

/*****************
 * GLSL helpers
 *****************/
const MAX_STRIPES = 256;
// Configurable parameter to control the number of stripes (lower = fewer, larger bars)
const DESIRED_STRIPE_COUNT = 6;

// Fullscreen quad (RawShader): position+uv
const VERT = /* glsl */ `
in vec3 position;
in vec2 uv;
out vec2 vUv;
void main(){
  vUv = uv;
  gl_Position = vec4(position, 1.0);
}`;

// Discrete stripes sampled from cumulative edges — outputs PREMULTIPLIED alpha
const STRIPES_FRAG = /* glsl */ `
precision highp float;
out vec4 fragColor;
in vec2 vUv;

uniform float u_time;
uniform vec2  u_resolution;
uniform vec3  u_bg;

uniform float u_speedPx;        // px/sec
uniform float u_periodPx;       // px for one repeat; constant across zoom
uniform float u_phasePx;        // px phase offset applied in screen space
uniform float u_softFrac;       // 0..0.5 of local stripe width
uniform int   u_colorCount;
uniform vec4  u_colors[${MAX_STRIPES}];
uniform float u_edges[${MAX_STRIPES + 1}];
// --- New uniforms for anti-repetition ---
uniform float u_noiseScale1Px;  // px scale for noise A
uniform float u_noiseScale2Px;  // px scale for noise B
uniform float u_colorJitter;    // 0..1 small jitter

// Cheap hash / value-noise helpers
float hash11(float n){ return fract(sin(n)*43758.5453123); }
float vnoise1(float x){
  float i = floor(x), f = fract(x);
  float a = hash11(i), b = hash11(i+1.0);
  float s = f*f*(3.0-2.0*f);
  return mix(a,b,s);
}

vec4 sampleColor(float idxF, int colorCount){
  if (colorCount <= 0) return vec4(0.0);
  float countF = float(colorCount);
  float wrapped = mod(idxF, countF);
  if (wrapped < 0.0) wrapped += countF;
  int i0 = int(floor(wrapped));
  int i1 = (i0 + 1) % colorCount;
  float t = fract(wrapped);
  vec4 a = u_colors[i0];
  vec4 b = u_colors[i1];
  return mix(a, b, t);
}

void main(){
  // Pixel-space sampling with a fixed stripe period so zoom doesn't scale stripes
  float xPx = vUv.x * u_resolution.x;
  float yPx = vUv.y * u_resolution.y;
  float P   = max(u_periodPx, 1.0);


  // Cell coordinate & local coord before warp
  float cellF = ((xPx + u_phasePx) - u_speedPx * u_time) / P;
  float x     = fract(cellF);


  // Pick stripe index with safe default = last (prevents wrap seam flicker)
  int colorCount = u_colorCount;
  if (colorCount <= 0){
    fragColor = vec4(u_bg, 1.0);
    return;
  }
  int last = colorCount - 1;
  int idx = last;
  for (int i = 0; i < ${MAX_STRIPES - 1}; ++i) {
    if (i >= colorCount) break;
    if (x < u_edges[i+1]) { idx = i; break; }
  }

  float invNoiseScale1 = 1.0 / max(u_noiseScale1Px, 1.0);
  float invNoiseScale2 = 1.0 / max(u_noiseScale2Px, 1.0);
  float noiseCoord = cellF + xPx * invNoiseScale1 + yPx * invNoiseScale2;
  float paletteShift = (vnoise1(noiseCoord * 0.31 + float(idx) * 0.17 + u_time * 0.03) - 0.5) * 2.0;
  float shiftedIdx = float(idx) + paletteShift * u_colorJitter * 0.9;

  vec4 c0 = sampleColor(shiftedIdx, colorCount);

  // edgePadPx is based on the brightness of the color being rendered. if the coloe
  float edgePadPx = clamp(c0.r * c0.g * c0.b * c0.a, 0.0, 1.0) * 60.0 + 10.0;

  if (yPx < edgePadPx || yPx > u_resolution.y - edgePadPx || xPx < edgePadPx || xPx > u_resolution.x - edgePadPx) {
    fragColor = vec4(mix(u_bg, vec3(1.0), 0.5), 1.0);
    return;
  }

  // Neighbor samples in the shifted palette space
  vec4 cL = sampleColor(shiftedIdx - 1.0, colorCount);
  vec4 cR = sampleColor(shiftedIdx + 1.0, colorCount);

  // Local stripe bounds & width
  float eL = u_edges[idx];
  float eR = u_edges[idx+1];
  float w  = max(eR - eL, 1e-6);

  // Feather widths: fraction of this stripe's width on each side
  float s = clamp(u_softFrac * w, 0.0, 0.5);

  // Distance to edges inside current stripe
  float distL = x - eL;
  float distR = eR - x;

  // Edge weights (1 at the edge, 0 past feather distance)
  float kL = 1.0 - smoothstep(0.0, s, distL);
  float kR = 1.0 - smoothstep(0.0, s, distR);

  // Base weight occupies the middle
  float w0 = max(1.0 - max(kL, kR), 0.0);

  // Small, continuous jitter to break exact color/weight repetition without jumps
  float jitter = (vnoise1(noiseCoord * 0.77 + float(idx) * 1.23 + u_time * 0.07) - 0.5) * 2.0 * u_colorJitter;
  // Skew edge weights slightly (one side fattens while the other thins)
  kL = max(kL * (1.0 + 0.45*jitter), 0.0);
  kR = max(kR * (1.0 - 0.45*jitter), 0.0);
  // Reduce center a touch when edges are emphasized
  w0 = max(w0 * (1.0 - 0.25*abs(jitter)), 0.0);
  // Renormalize
  float norm = max(kL + kR + w0, 1e-6);
  kL /= norm; kR /= norm; w0 /= norm;

  // Subtle neighbor tint drift based on jitter sign
  float nBias = 0.5 + 0.5*sign(jitter);
  vec3  nMix  = mix(cL.rgb, cR.rgb, nBias);

  float aMix = c0.a*w0 + cL.a*kL + cR.a*kR;
  vec3  rgb  = (c0.rgb*c0.a*w0 + cL.rgb*cL.a*kL + cR.rgb*cR.a*kR) / max(aMix, 1e-6);
  // Gently blend towards neighbor mix when jitter present
  rgb = mix(rgb, nMix, 0.25*abs(jitter));

  vec3 outColor = mix(u_bg, rgb, aMix);

  fragColor = vec4(outColor, 1.0);
}`;

// Dual Kawase downsample/upsample — preserves premultiplied alpha
const KAWASE_DOWN_FRAG = /* glsl */ `
precision highp float;
out vec4 fragColor;
in vec2 vUv;
uniform sampler2D tInput;
uniform vec2 u_texel;   // 1 / sourceSize
uniform float u_offset; // per level
void main(){
  vec2 o = u_offset * u_texel;
  vec4 c = texture(tInput, vUv) * 0.25;
  c += texture(tInput, vUv + vec2( o.x,  o.y)) * 0.1875;
  c += texture(tInput, vUv + vec2(-o.x,  o.y)) * 0.1875;
  c += texture(tInput, vUv + vec2( o.x, -o.y)) * 0.1875;
  c += texture(tInput, vUv + vec2(-o.x, -o.y)) * 0.1875;
  fragColor = c; // premultiplied RGBA
}`;

const KAWASE_UP_FRAG = /* glsl */ `
precision highp float;
out vec4 fragColor;
in vec2 vUv;
uniform sampler2D tInput;
uniform vec2 u_texel;   // 1 / sourceSize
uniform float u_offset; // per level
void main(){
  vec2 o = u_offset * u_texel;
  vec4 c = texture(tInput, vUv) * 0.5;
  c += texture(tInput, vUv + vec2( o.x,  o.y)) * 0.125;
  c += texture(tInput, vUv + vec2(-o.x,  o.y)) * 0.125;
  c += texture(tInput, vUv + vec2( o.x, -o.y)) * 0.125;
  c += texture(tInput, vUv + vec2(-o.x, -o.y)) * 0.125;
  fragColor = c; // premultiplied RGBA
}`;

// Composite: grain + optional spotlight overlay (keeps premultiplied alpha)
const COMP_FRAG = /* glsl */ `
precision highp float;
out vec4 fragColor;
in vec2 vUv;
uniform sampler2D tInput;
uniform vec2  u_resolution;   // region px
uniform vec2  u_origin;       // NEW: viewport origin in device px
uniform float u_time;
uniform float u_grain;        // 0..1
uniform vec3  u_bg;
uniform float u_radiusPx;
uniform float u_edgePadPx;    // NEW: outward pad in px
uniform bool  u_fadeStart;    // NEW: whether to apply fade from left
uniform float u_fadeWidthPx;  // NEW: width of fade in pixels
uniform float u_fadeEndPx;    // NEW: pixel position where fade should end (relative to viewport)

uniform bool  u_spotEnabled;
uniform vec2  u_spotMouse;    // local px coords (origin top-left in this pass)
uniform float u_spotSize;     // px
uniform float u_spotOpacity;  // 0..1

float hash(vec2 p){ p=vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3)));
  return fract(sin(p.x+p.y)*43758.5453123); }
vec3 overlay(vec3 base, vec3 blend){
  return mix(2.0*base*blend, 1.0 - 2.0*(1.0-base)*(1.0-blend), step(0.5, base)); }

// Analytic rounded-rect SDF in pixel space
float sdRoundRect(vec2 p, vec2 b, float r) {
  vec2 q = abs(p) - b + vec2(r);
  return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
}

void main(){
  vec4 tex = texture(tInput, vUv);   // premultiplied
  float a = tex.a;
  if (a <= 0.0){ fragColor = vec4(0.0); return; }
  vec3 col = tex.rgb / max(a, 1e-6);

  // --- Pixel-space SDF aligned to the viewport ---
  vec2 fragPx   = gl_FragCoord.xy - u_origin;          // local px (pixel centers)
  vec2 halfSize = 0.5 * u_resolution;
  float d  = sdRoundRect(fragPx - halfSize, halfSize + vec2(u_edgePadPx), u_radiusPx);
  float aa = fwidth(d);

  // Wider, symmetric AA ramp to kill 1px shimmering at some scales
  float mask = 1.0 - smoothstep(-0.5*aa, 1.5*aa, d);
  a *= mask;
  if (a <= 0.0){ fragColor = vec4(0.0); return; }

  // Edge-only fade for very dark colors (prevents dark rim on blur)
  float edge = smoothstep(0.0, 2.5*aa, d);
  float luma = dot(col, vec3(0.2126, 0.7152, 0.0722));
  float darkFade = smoothstep(${DARK_EDGE_LUMA_MIN}, ${DARK_EDGE_LUMA_MAX}, luma);
  a *= mix(1.0, darkFade, edge);
  if (a <= 0.0){ fragColor = vec4(0.0); return; }

  // Grain
  float g = hash(fragPx + u_time*60.0) * 2.0 - 1.0;
  vec3  grain = vec3(0.5 + 0.5*g);
  col = mix(col, overlay(col, grain), clamp(u_grain, 0.0, 1.0));

  // Spotlight (if enabled)
  if (u_spotEnabled){
    vec2 pp = vec2(fragPx.x, (u_resolution.y - fragPx.y));
    float d2 = distance(pp, u_spotMouse);
    float r  = max(u_spotSize, 1.0);
    float k  = smoothstep(r, 0.0, d2);
    vec3 spot = mix(u_bg, vec3(1.0), 0.65);
    vec3 blended = overlay(col, spot);
    col = mix(col, blended, k * u_spotOpacity);
  }

  // Left fade (if enabled)
  if (u_fadeStart && u_fadeWidthPx > 0.0) {
    // fragPx.x and u_fadeEndPx are both relative to viewport start
    // We want fade to end at u_fadeEndPx and extend u_fadeWidthPx left of that
    float fadeStartPx = u_fadeEndPx - u_fadeWidthPx;
    float fadeFactor = smoothstep(fadeStartPx, u_fadeEndPx, fragPx.x);
    a *= fadeFactor;
    if (a <= 0.0){ fragColor = vec4(0.0); return; }
  }

  fragColor = vec4(col * a, a); // premultiplied out
}`;

/*****************
 * Utilities
 *****************/
const hexToRgb = (hex: `#${string}`) => {
  const h = hex.replace('#', '');
  return [
    parseInt(h.slice(0, 2), 16) / 255,
    parseInt(h.slice(2, 4), 16) / 255,
    parseInt(h.slice(4, 6), 16) / 255,
  ] as [number, number, number];
};
const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
const easeInOutSine = (t: number) => 0.5 - 0.5 * Math.cos(Math.PI * t);

function buildStripeSequence(
  base: Array<{ hex: `#${string}`; weight: number }>,
  mixIntensity: number
) {
  const seq: Array<{ color: [number, number, number]; isSolid: boolean }> = [];
  if (!base.length) return seq;
  for (let i = 0; i < base.length; i++) {
    const cur = base[i];
    const nxt = base[(i + 1) % base.length];
    const solidCount = Math.floor(Math.max(0, cur.weight));
    for (let j = 0; j < solidCount; j++)
      seq.push({ color: hexToRgb(cur.hex), isSolid: true });
    for (let k = 0; k < mixIntensity; k++) {
      const t = (k + 1) / (mixIntensity + 1);
      const a = hexToRgb(cur.hex),
        b = hexToRgb(nxt.hex);
      seq.push({
        color: [lerp(a[0], b[0], t), lerp(a[1], b[1], t), lerp(a[2], b[2], t)],
        isSolid: false,
      });
    }
  }
  return seq;
}
function seededNumber(s: string): number {
  let h = 0;
  for (let i = 0; i < s.length; i++) {
    h = (h << 5) - h + s.charCodeAt(i);
    h |= 0;
  }
  return (Math.abs(h) / 0xffffffff) * 100_000 + 1.0;
}

/**********************
 * Region Renderer
 **********************/
class StripesRegion {
  private three: THREE.WebGLRenderer;

  // Scene shared by all passes; we swap materials on the same full-screen mesh.
  private scene = new THREE.Scene();
  private camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
  private quad: THREE.Mesh<THREE.BufferGeometry, THREE.RawShaderMaterial>;
  private fsGeo: THREE.BufferGeometry;

  // Materials
  private stripesMat: THREE.RawShaderMaterial;
  private kawaseDownMat: THREE.RawShaderMaterial;
  private kawaseUpMat: THREE.RawShaderMaterial;
  private compMat: THREE.RawShaderMaterial;

  // Uniforms
  private stripesU: Record<string, THREE.IUniform>;
  private compU: Record<string, THREE.IUniform>;

  // Data
  private colorsArray = new Float32Array(MAX_STRIPES * 4);
  private edgesArray = new Float32Array(MAX_STRIPES + 1);
  private activeColorSlots = 0;
  private activeEdgeSlots = 0;
  private periodPx = 1; // current period in device pixels (updated per-frame based on zoom)
  private periodBeats = STRIPE_PERIOD_BEATS; // effective beats per period (may scale for low zoom)
  private phasePx = 0; // current phase in device pixels (updated per-frame based on timeline position)
  private phaseInitialized = false;
  private instancePhaseFrac = 0; // random per-instance offset (0..1 of base period)
  private anchorBeats = 0; // absolute timeline beats used as anchor for stripes

  private stripes: Array<{
    color: [number, number, number];
    isSolid: boolean;
    baseWidthPx: number; // base width in px (before animation)
    widthPx: number; // current width in px (after animation)
    widthAnim: null | { start: number; dur: number; from: number; to: number };
    colorAnim: null | {
      start: number;
      dur: number;
      from: [number, number, number];
      to: [number, number, number];
    };
    alpha: number;
    alphaAnim: null | { start: number; dur: number; from: number; to: number };
  }> = [];
  private seed: number;
  private rng!: () => number;
  private rngInitialized = false;

  // RTs
  private rtFull?: THREE.WebGLRenderTarget;
  private downRT: THREE.WebGLRenderTarget[] = [];
  private downDims: Array<{ w: number; h: number }> = [];
  private levels = 1;

  private lastW = 0;
  private lastH = 0;
  private t0 = performance.now() / 1000;

  private mutationTimer: number | null = null;
  private breathingTimer: number | null = null;
  private activeTimeouts = new Set<number>(); // Track active setTimeout IDs for cleanup

  // Internal RAF handle to drive continuous animation
  private _animRaf: number | null = null;
  private requestRedraw: (() => void) | null = null;

  // Spotlight coords (local to viewport in device px)
  private spotMouse = new THREE.Vector2(0, 0);
  private spotEnabled = SPOTLIGHT.enabled;
  public stripePalette: Array<{ hex: `#${string}`; weight: number }>;

  constructor(
    three: THREE.WebGLRenderer,
    seed: number,
    stripePalette: Array<{ hex: `#${string}`; weight: number }>
  ) {
    this.three = three;
    this.seed = seed;
    this.stripePalette = stripePalette;
    this.initRng();

    // Random offset in period (0..1) for visual variety between clips
    this.instancePhaseFrac = this.rng();

    // Fullscreen quad (for offscreen passes)
    this.fsGeo = new THREE.BufferGeometry();
    const pos = new Float32Array([
      -1, -1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, 1, 1, 0, -1, 1, 0,
    ]);
    const uvs = new Float32Array([0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1]);
    this.fsGeo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
    this.fsGeo.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));

    // Stripes uniforms
    this.stripesU = {
      u_time: { value: 0 },
      u_resolution: { value: new THREE.Vector2(1, 1) },
      u_bg: {
        value: new THREE.Color(
          lighten(this.stripePalette[0].hex.slice(0, 7), 0.2).slice(0, 7)
        )
          .convertSRGBToLinear()
          .toArray()
          .slice(0, 3),
      },
      u_speedPx: { value: 14.0 }, // updated per-viewport
      u_periodPx: { value: 1 }, // updated per-frame in setViewport based on zoom
      u_phasePx: { value: 0.0 }, // updated per-frame in setViewport based on timeline position
      u_softFrac: { value: SOFT_EDGE_FRAC },
      u_colorCount: { value: 0 },
      u_colors: { value: this.colorsArray },
      u_edges: { value: this.edgesArray },
      // new anti-repetition uniforms
      u_noiseScale1Px: { value: NOISE_SCALE_1_PX },
      u_noiseScale2Px: { value: NOISE_SCALE_2_PX },
      u_colorJitter: { value: COLOR_JITTER },
    };

    this.stripesMat = new THREE.RawShaderMaterial({
      glslVersion: THREE.GLSL3,
      vertexShader: VERT,
      fragmentShader: STRIPES_FRAG,
      uniforms: this.stripesU,
      depthTest: false,
      depthWrite: false,
      transparent: false,
    });

    this.kawaseDownMat = new THREE.RawShaderMaterial({
      glslVersion: THREE.GLSL3,
      vertexShader: VERT,
      fragmentShader: KAWASE_DOWN_FRAG,
      uniforms: {
        tInput: { value: null },
        u_texel: { value: new THREE.Vector2(1, 1) },
        u_offset: { value: 1.5 },
      },
      depthTest: false,
      depthWrite: false,
      transparent: false,
    });

    this.kawaseUpMat = new THREE.RawShaderMaterial({
      glslVersion: THREE.GLSL3,
      vertexShader: VERT,
      fragmentShader: KAWASE_UP_FRAG,
      uniforms: {
        tInput: { value: null },
        u_texel: { value: new THREE.Vector2(1, 1) },
        u_offset: { value: 1.5 },
      },
      depthTest: false,
      depthWrite: false,
      transparent: false,
    });

    this.compU = {
      tInput: { value: null },
      u_resolution: { value: new THREE.Vector2(1, 1) },
      u_origin: { value: new THREE.Vector2(0, 0) }, // NEW
      u_time: { value: 0 },
      u_grain: { value: GRAIN_STRENGTH },
      u_bg: {
        value: new THREE.Color(this.stripePalette[0].hex.slice(0, 7))
          .convertSRGBToLinear()
          .toArray()
          .slice(0, 3),
      },
      u_radiusPx: { value: clipCornerRadius },
      u_edgePadPx: { value: EDGE_PAD_PX }, // NEW
      u_fadeStart: { value: false }, // NEW
      u_fadeWidthPx: { value: 60.0 }, // NEW
      u_fadeEndPx: { value: 0.0 }, // NEW
      u_spotEnabled: { value: this.spotEnabled },
      u_spotMouse: { value: new THREE.Vector2(0, 0) },
      u_spotSize: { value: SPOTLIGHT.size },
      u_spotOpacity: { value: SPOTLIGHT.opacity },
    };

    this.compMat = new THREE.RawShaderMaterial({
      glslVersion: THREE.GLSL3,
      vertexShader: VERT,
      fragmentShader: COMP_FRAG,
      uniforms: this.compU,
      depthTest: false,
      depthWrite: false,
      transparent: true,
      premultipliedAlpha: true,
      blending: THREE.CustomBlending,
      blendEquation: THREE.AddEquation,
      blendEquationAlpha: THREE.AddEquation,
      blendSrc: THREE.OneFactor,
      blendDst: THREE.OneMinusSrcAlphaFactor,
      blendSrcAlpha: THREE.OneFactor,
      blendDstAlpha: THREE.OneMinusSrcAlphaFactor,
    });

    this.quad = new THREE.Mesh(this.fsGeo, this.stripesMat);
    this.scene.add(this.quad);

    // init
    this.rebuild(1, 1);
    this.startTimers();
  }

  private initRng() {
    if (this.rngInitialized) return;
    // Mulberry32 RNG for deterministic widths
    const makeRng = (s: number) => {
      let t = (Math.floor(s * 1000) ^ 0x6d2b79f5) >>> 0;
      return () => {
        t += 0x6d2b79f5;
        let r = Math.imul(t ^ (t >>> 15), 1 | t);
        r ^= r + Math.imul(r ^ (r >>> 7), 61 | r);
        return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
      };
    };
    this.rng = makeRng(this.seed);
    this.rngInitialized = true;
  }

  private startTimers() {
    // Configurable animation frequencies (in milliseconds)

    // Match CSS cadence: mutate tick every MUTATION_INTERVAL_MS with ~70% chance; breathe every BREATHING_INTERVAL_MS with ~70% chance
    this.mutationTimer ??= window.setInterval(
      () => this.maybeMutate(),
      MUTATION_INTERVAL_MS
    );
    this.breathingTimer ??= window.setInterval(
      () => this.maybeBreathe(),
      BREATHING_INTERVAL_MS
    );
  }
  private stopTimers() {
    if (this.mutationTimer != null) window.clearInterval(this.mutationTimer);
    if (this.breathingTimer != null) window.clearInterval(this.breathingTimer);
    this.mutationTimer = this.breathingTimer = null;
    // Clear all active timeouts
    this.activeTimeouts.forEach((id) => window.clearTimeout(id));
    this.activeTimeouts.clear();
  }

  public assignStripePalette(
    stripePalette: Array<{ hex: `#${string}`; weight: number }>
  ) {
    if (this.stripePalette === stripePalette) return;
    this.stripePalette = stripePalette;
    this.buildStripes(this.periodPx);
    this.stripesU.u_bg.value = new THREE.Color(
      this.stripePalette[0].hex.slice(0, 7)
    )
      .toArray()
      .slice(0, 3);
    this.compU.u_bg.value = new THREE.Color(
      this.stripePalette[0].hex.slice(0, 7)
    )
      .toArray()
      .slice(0, 3);
  }

  private buildStripes(periodPx: number) {
    const seq = buildStripeSequence(this.stripePalette, STRIPES_MIX_INTENSITY);
    const count = Math.min(
      seq.length,
      Math.min(MAX_STRIPES, DESIRED_STRIPE_COUNT)
    );

    if (count === 0) {
      this.stripes = [];
      this.uploadColors();
      this.uploadEdges();
      (this.stripesU.u_periodPx as THREE.IUniform).value = Math.max(
        1,
        periodPx
      );
      return;
    }

    // Build stripes with equal distribution and slight variation for visual interest
    // Note: periodPx here is just used for initial relative sizing - the actual period
    // is updated dynamically in setViewport based on zoom level, and stripes are normalized
    const baseWidth = Math.max(1, periodPx) / count;
    const variation = Math.min(baseWidth * 0.2, 50); // Max 20% variation

    this.stripes = seq.slice(0, count).map((s) => {
      // Add small deterministic variation to avoid perfect uniformity
      const variationOffset = (this.rng() - 0.5) * variation * 2;
      const baseW = baseWidth + variationOffset;
      return {
        color: s.color.slice() as [number, number, number],
        isSolid: s.isSolid,
        baseWidthPx: baseW,
        widthPx: baseW,
        widthAnim: null,
        colorAnim: null,
        alpha: 1,
        alphaAnim: null,
      };
    });

    this.uploadColors();
    this.uploadEdges(); // initial layout
    (this.stripesU.u_periodPx as THREE.IUniform).value = Math.max(1, periodPx);
  }

  private uploadColors() {
    const count = Math.min(this.stripes.length, MAX_STRIPES);
    for (let i = 0; i < count; i++) {
      const base = i * 4;
      const s = this.stripes[i];
      this.colorsArray[base + 0] = s.color[0];
      this.colorsArray[base + 1] = s.color[1];
      this.colorsArray[base + 2] = s.color[2];
      this.colorsArray[base + 3] = s.alpha;
    }
    if (this.activeColorSlots > count) {
      this.colorsArray.fill(0, count * 4, this.activeColorSlots * 4);
    }
    this.activeColorSlots = count;
    (this.stripesU.u_colorCount as THREE.IUniform).value = count;
    this.stripesU.u_colors.value = this.colorsArray;
    this.stripesMat.uniformsNeedUpdate = true;
  }
  private uploadEdges() {
    const count = Math.min(this.stripes.length, MAX_STRIPES);
    let sum = 0;
    for (let i = 0; i < count; i++) sum += this.stripes[i].widthPx;
    if (sum <= 0) sum = 1;
    let acc = 0;
    this.edgesArray[0] = 0.0;
    for (let i = 0; i < count; i++) {
      acc += this.stripes[i].widthPx / sum; // normalize
      this.edgesArray[i + 1] = acc;
    }
    if (this.activeEdgeSlots > count) {
      this.edgesArray.fill(1.0, count + 1, this.activeEdgeSlots + 1);
    }
    this.activeEdgeSlots = count;
    this.stripesMat.uniformsNeedUpdate = true;
    this.stripesU.u_edges.value = this.edgesArray;
  }

  private setupTargets(W: number, H: number) {
    this.rtFull?.dispose();
    for (const rt of this.downRT) rt.dispose();
    this.downRT = [];
    this.downDims = [];

    this.rtFull = new THREE.WebGLRenderTarget(W, H, {
      type: THREE.UnsignedByteType,
    });
    this.rtFull.texture.colorSpace = THREE.SRGBColorSpace;

    // Requested blur strength -> max theoretical levels
    const reqLevels = kawaseLevelsFromBlur(BLUR_PX);

    // Clamp by actual viewport so we never downsample below MIN_KAWASE_RT_PX
    let w = W,
      h = H,
      levelsBySize = 0;
    for (let i = 0; i < reqLevels; i++) {
      const nw = Math.max(1, w >> 1);
      const nh = Math.max(1, h >> 1);
      if (nw < MIN_KAWASE_RT_PX || nh < MIN_KAWASE_RT_PX) break;
      levelsBySize++;
      w = nw;
      h = nh;
    }
    this.levels = levelsBySize;

    for (let i = 0, dw = W, dh = H; i < this.levels; i++) {
      dw = Math.max(1, dw >> 1);
      dh = Math.max(1, dh >> 1);
      const rt = new THREE.WebGLRenderTarget(dw, dh, {
        type: THREE.UnsignedByteType,
      });
      rt.texture.colorSpace = THREE.SRGBColorSpace;
      this.downRT.push(rt);
      this.downDims.push({ w: dw, h: dh });
    }
  }

  private rebuild(W: number, H: number) {
    const isFirstBuild = this.lastW === 0 || this.lastH === 0;
    this.lastW = W;
    this.lastH = H;

    (this.stripesU.u_resolution.value as THREE.Vector2).set(W, H);
    (this.compU.u_resolution.value as THREE.Vector2).set(W, H);

    this.setupTargets(W, H);

    // Only build stripes on first initialization or if we don't have any
    if (isFirstBuild || this.stripes.length === 0) {
      this.buildStripes(this.periodPx); // Initialize stripe distribution (period updated per-frame in setViewport)
    } else {
      // Just update the layout for existing stripes
      this.updateStripeLayout(W);
    }
  }

  private updateStripeLayout(_viewW: number) {
    // Refresh normalized edge positions (actual pixel widths determined by period in setViewport)
    this.uploadEdges();
  }

  setViewport(
    vpW: number,
    vpH: number,
    radiusPx: number,
    localMousePx?: { x: number; y: number },
    originPx?: { x: number; y: number },
    timelineBeatsAtViewportLeft?: number,
    pxPerBeat?: number,
    dpr?: number
  ) {
    const roundedW = Math.max(1, Math.round(vpW));
    const roundedH = Math.max(1, Math.round(vpH));

    // Calculate period and phase in timeline space if parameters provided
    if (
      timelineBeatsAtViewportLeft !== undefined &&
      pxPerBeat !== undefined &&
      dpr !== undefined
    ) {
      const basePeriodPx = STRIPE_PERIOD_BEATS * pxPerBeat * dpr;

      // Power-of-2 multiplier for clean transitions at low zoom
      let multiplier = 1;
      while (
        basePeriodPx * multiplier < TARGET_STRIPE_PERIOD_DEVICE_PX / 2 &&
        multiplier < MAX_STRIPE_PERIOD_MULTIPLIER
      ) {
        multiplier *= 2;
      }

      const effectiveBeats = STRIPE_PERIOD_BEATS * multiplier;
      this.periodBeats = effectiveBeats;

      // Fixed visual period when zoomed out to prevent scaling
      if (multiplier > 1) {
        this.periodPx = TARGET_STRIPE_PERIOD_DEVICE_PX;
      } else {
        this.periodPx = Math.max(1, basePeriodPx);
      }

      // Adjust pixel factor based on whether we're using fixed period
      const pxFactor =
        multiplier > 1 ? this.periodPx / effectiveBeats : pxPerBeat * dpr;

      const instanceOffsetBeats = this.instancePhaseFrac * STRIPE_PERIOD_BEATS;
      const pivotDevicePx = Math.max(
        0,
        Math.min(localMousePx?.x ?? roundedW * 0.5, roundedW)
      );
      const pivotBeatsOffset = pivotDevicePx / Math.max(dpr, 1e-6) / pxPerBeat;
      const pivotBeats = timelineBeatsAtViewportLeft + pivotBeatsOffset;

      // Calculate position relative to anchor
      const timelinePositionFromAnchor =
        pivotBeats - this.anchorBeats + instanceOffsetBeats;

      // Wrap to effective period
      const effectivePosition =
        ((timelinePositionFromAnchor % effectiveBeats) + effectiveBeats) %
        effectiveBeats;

      // Convert to phase in pixels
      const patternPositionPx = effectivePosition * pxFactor;
      this.phasePx = patternPositionPx - pivotDevicePx;

      this.phaseInitialized = true;
    }

    // Update spotlight position if we have a live mouse coordinate
    if (localMousePx) {
      const clampedX = Math.max(0, Math.min(localMousePx.x, roundedW));
      const clampedY = Math.max(0, Math.min(localMousePx.y, roundedH));
      this.spotMouse.set(clampedX, clampedY);
    }

    if (originPx) {
      (this.compU.u_origin.value as THREE.Vector2).set(originPx.x, originPx.y);
    }

    const needsRebuild =
      Math.abs(roundedW - this.lastW) > 5 ||
      Math.abs(roundedH - this.lastH) > 5;
    if (needsRebuild) {
      this.rebuild(roundedW, roundedH);
    } else if (roundedW !== this.lastW || roundedH !== this.lastH) {
      (this.stripesU.u_resolution.value as THREE.Vector2).set(
        roundedW,
        roundedH
      );
      (this.compU.u_resolution.value as THREE.Vector2).set(roundedW, roundedH);
    }

    (this.compU.u_radiusPx as THREE.IUniform).value = radiusPx;
    (this.stripesU.u_periodPx as THREE.IUniform).value = this.periodPx;
    (this.stripesU.u_speedPx as THREE.IUniform).value =
      this.periodPx / SCROLL_SECONDS_PER_WIDTH;
    (this.stripesU.u_phasePx as THREE.IUniform).value = this.phasePx;
    (this.stripesU.u_noiseScale1Px as THREE.IUniform).value = NOISE_SCALE_1_PX;
    (this.stripesU.u_noiseScale2Px as THREE.IUniform).value = NOISE_SCALE_2_PX;
    (this.stripesU.u_colorJitter as THREE.IUniform).value = COLOR_JITTER;
  }

  enableSpotlight(enabled: boolean) {
    this.spotEnabled = enabled;
  }

  setFadeStart(
    fadeStart: boolean,
    fadeWidthPx: number = 60.0,
    fadeEndPx: number = 0.0
  ) {
    (this.compU.u_fadeStart as THREE.IUniform).value = fadeStart;
    (this.compU.u_fadeWidthPx as THREE.IUniform).value = fadeWidthPx;
    (this.compU.u_fadeEndPx as THREE.IUniform).value = fadeEndPx;
  }

  setAnchorBeats(beats: number) {
    if (!Number.isFinite(beats)) return;
    this.anchorBeats = beats;
  }

  setRedrawCallback(cb?: () => void) {
    this.requestRedraw = cb ?? null;
    this._startAnimationLoop();
  }

  private _startAnimationLoop() {
    if (this._animRaf === null) {
      const tick = () => {
        this.requestRedraw?.();
        this._animRaf = requestAnimationFrame(tick);
      };
      this._animRaf = requestAnimationFrame(tick);
    }
  }

  private _stopAnimationLoop() {
    if (this._animRaf !== null) {
      cancelAnimationFrame(this._animRaf);
      this._animRaf = null;
    }
  }

  render() {
    const t = performance.now() / 1000 - this.t0;
    this.stripesU.u_time.value = t + this.seed; // Add seed for variation between instances
    this.compU.u_time.value = t;

    // --- Save FB state (viewport/scissor) set by caller ---
    const prevViewport = new THREE.Vector4();
    const prevScissor = new THREE.Vector4();
    const prevScissorTest = this.three.getScissorTest();
    this.three.getViewport(prevViewport);
    this.three.getScissor(prevScissor);

    // Animate width / color / alpha (CSS-like)
    let colorsChanged = false;
    let widthsChanged = false;
    for (let i = 0; i < this.stripes.length; i++) {
      const s = this.stripes[i];
      if (s.widthAnim) {
        const p = Math.min(1, (t - s.widthAnim.start) / s.widthAnim.dur);
        const k = easeInOutSine(p);
        const scale = lerp(s.widthAnim.from, s.widthAnim.to, k);
        s.widthPx = Math.max(1, s.baseWidthPx * scale);
        if (p >= 1) s.widthAnim = null;
        widthsChanged = true;
      }
      if (s.colorAnim) {
        const p = Math.min(1, (t - s.colorAnim.start) / s.colorAnim.dur);
        const k = easeInOutSine(p);
        s.color[0] = lerp(s.colorAnim.from[0], s.colorAnim.to[0], k);
        s.color[1] = lerp(s.colorAnim.from[1], s.colorAnim.to[1], k);
        s.color[2] = lerp(s.colorAnim.from[2], s.colorAnim.to[2], k);
        if (p >= 1) s.colorAnim = null;
        colorsChanged = true;
      }
      if (s.alphaAnim) {
        const p = Math.min(1, (t - s.alphaAnim.start) / s.alphaAnim.dur);
        const k = easeInOutSine(p);
        s.alpha = lerp(s.alphaAnim.from, s.alphaAnim.to, k);
        if (p >= 1) s.alphaAnim = null;
        colorsChanged = true;
      }
    }
    if (colorsChanged) this.uploadColors();
    if (widthsChanged) this.uploadEdges();
    // NOTE: Even if not changing this frame, width can be static; layout stays in u_edges.

    // --- Pipeline ---
    // IMPORTANT: disable scissor and use RT-sized viewport for offscreen passes
    this.three.setScissorTest(false);

    // Pass 1: stripes -> rtFull
    this.quad.geometry = this.fsGeo;
    this.quad.material = this.stripesMat;
    this.three.setRenderTarget(this.rtFull!);
    this.three.setViewport(0, 0, this.lastW, this.lastH);
    this.three.setScissor(0, 0, this.lastW, this.lastH);
    this.three.render(this.scene, this.camera);

    // Pass 2: Kawase downsample chain
    let srcTex = this.rtFull!.texture;
    let srcW = this.lastW,
      srcH = this.lastH;
    for (let i = 0; i < this.levels; i++) {
      const rt = this.downRT[i];
      (this.kawaseDownMat.uniforms.tInput as THREE.IUniform).value = srcTex;
      (this.kawaseDownMat.uniforms.u_texel as THREE.IUniform).value.set(
        1 / srcW,
        1 / srcH
      );
      (this.kawaseDownMat.uniforms.u_offset as THREE.IUniform).value =
        1.5 + i * 0.5;
      this.quad.geometry = this.fsGeo;
      this.quad.material = this.kawaseDownMat;
      this.three.setRenderTarget(rt);
      this.three.setViewport(0, 0, this.downDims[i].w, this.downDims[i].h);
      this.three.setScissor(0, 0, this.downDims[i].w, this.downDims[i].h);
      this.three.render(this.scene, this.camera);
      srcTex = rt.texture;
      srcW = this.downDims[i].w;
      srcH = this.downDims[i].h;
    }

    // Pass 3: Kawase upsample chain -> rtFull
    for (let i = this.levels - 1; i >= 0; i--) {
      const target = i > 0 ? this.downRT[i - 1] : this.rtFull!;
      (this.kawaseUpMat.uniforms.tInput as THREE.IUniform).value = srcTex;
      (this.kawaseUpMat.uniforms.u_texel as THREE.IUniform).value.set(
        1 / srcW,
        1 / srcH
      );
      (this.kawaseUpMat.uniforms.u_offset as THREE.IUniform).value =
        1.5 + i * 0.5;
      this.quad.geometry = this.fsGeo;
      this.quad.material = this.kawaseUpMat;
      this.three.setRenderTarget(target);
      const tw = i > 0 ? this.downDims[i - 1].w : this.lastW;
      const th = i > 0 ? this.downDims[i - 1].h : this.lastH;
      this.three.setViewport(0, 0, tw, th);
      this.three.setScissor(0, 0, tw, th);
      this.three.render(this.scene, this.camera);
      srcTex = target.texture;
      if (i > 0) {
        srcW = this.downDims[i - 1].w;
        srcH = this.downDims[i - 1].h;
      } else {
        srcW = this.lastW;
        srcH = this.lastH;
      }
    }

    // Pass 4: composite -> screen
    (this.compU.tInput as THREE.IUniform).value = this.rtFull!.texture;
    (this.compU.u_spotEnabled as THREE.IUniform).value = !!this.spotEnabled;
    (this.compU.u_spotMouse as THREE.IUniform).value.copy(this.spotMouse);
    (this.compU.u_spotSize as THREE.IUniform).value = SPOTLIGHT.size;
    (this.compU.u_spotOpacity as THREE.IUniform).value = SPOTLIGHT.opacity;

    this.quad.geometry = this.fsGeo; // Use fullscreen quad, let shader SDF do the masking
    this.quad.material = this.compMat;
    // Restore caller's viewport/scissor for the on-screen composite
    this.three.setRenderTarget(null);
    this.three.setViewport(
      prevViewport.x,
      prevViewport.y,
      prevViewport.z,
      prevViewport.w
    );
    this.three.setScissor(
      prevScissor.x,
      prevScissor.y,
      prevScissor.z,
      prevScissor.w
    );
    this.three.setScissorTest(prevScissorTest);
    this.three.render(this.scene, this.camera);
  }

  private maybeMutate() {
    if (this.stripes.length === 0) return;
    // ~70% chance per 200ms tick (same as CSS settings)
    if (Math.random() > 0.7) return;
    const solids = this.stripes
      .map((s, i) => (s.isSolid ? i : -1))
      .filter((i) => i >= 0);
    if (!solids.length) return;
    const idx = solids[(Math.random() * solids.length) | 0];
    const s = this.stripes[idx];
    const palette = this.stripePalette.map((c) =>
      hexToRgb(c.hex.slice(0, 7) as `#${string}`)
    );
    let target = palette[(Math.random() * palette.length) | 0];
    let guard = 0;
    while (
      target[0] === s.color[0] &&
      target[1] === s.color[1] &&
      target[2] === s.color[2] &&
      guard++ < 8
    ) {
      target = palette[(Math.random() * palette.length) | 0];
    }
    s.colorAnim = {
      start: performance.now() / 1000 - this.t0,
      dur: 3.2, // CSS --mutation-speed
      from: s.color.slice() as [number, number, number],
      to: target.slice() as [number, number, number],
    };
  }
  private maybeBreathe() {
    if (this.stripes.length === 0) return;
    // ~70% chance per 100ms tick (CSS breathing.chance)
    if (Math.random() > 0.7) return;
    const idx = (Math.random() * this.stripes.length) | 0;
    const s = this.stripes[idx];
    if (s.widthAnim) return; // don't stack

    const now = performance.now() / 1000 - this.t0;
    const BREATH_SPEED = 3.0; // seconds (CSS --breathing-speed) - slowed down
    const INTENSITY = 2.0; // flex-basis multiplier (CSS intensity) - reduced
    const FADE_CHANCE = 0.6; // optional fade sync - reduced frequency

    // Expand
    s.widthAnim = { start: now, dur: BREATH_SPEED, from: 1.0, to: INTENSITY };
    // Optional fade-out during expansion + fade-in back
    if (!s.alphaAnim && Math.random() < FADE_CHANCE) {
      s.alphaAnim = { start: now, dur: BREATH_SPEED, from: 1.0, to: 0.0 };
      const fadeTimeoutId = window.setTimeout(() => {
        this.activeTimeouts.delete(fadeTimeoutId);
        const back = performance.now() / 1000 - this.t0;
        s.alphaAnim = { start: back, dur: BREATH_SPEED, from: 0.0, to: 1.0 };
      }, BREATH_SPEED * 1000);
      this.activeTimeouts.add(fadeTimeoutId);
    }
    // Contract back after one breath
    const contractTimeoutId = window.setTimeout(() => {
      this.activeTimeouts.delete(contractTimeoutId);
      const back = performance.now() / 1000 - this.t0;
      s.widthAnim = {
        start: back,
        dur: BREATH_SPEED,
        from: INTENSITY,
        to: 1.0,
      };
    }, BREATH_SPEED * 1000);
    this.activeTimeouts.add(contractTimeoutId);
  }

  dispose() {
    this.stopTimers();
    this._stopAnimationLoop();
    this.fsGeo.dispose();
    this.stripesMat.dispose();
    this.kawaseDownMat.dispose();
    this.kawaseUpMat.dispose();
    this.compMat.dispose();
    this.rtFull?.dispose();
    for (const rt of this.downRT) rt.dispose();
    this.scene.clear();
  }
}

const lastAvailableDurations: Record<string, number> = {};

/***********************
 * Public entry point
 ***********************/
export default function makeClipGenerationWebglRegions(
  studioContext: StudioContextType
): CanvasRegion[] {
  const webGLCache = studioContext.webGLCache.current;
  const cacheSet = new Set<string>();
  const regions: CanvasRegion[] = [];

  const songEndBeats = getSongEndBeats(studioContext.state);
  const songStartBeats = getSongStartBeats(studioContext.state);

  const makeKey = (trackId: string, startBeats?: number, endBeats?: number) =>
    `${CACHE_KEY}:${trackId}:${startBeats}:${endBeats}`;

  const tracks = getTracksAndExpandedTakeLanes(studioContext.state);
  const takeLaneParentsByTrackId = getTakeLaneParentsByTrackId(
    studioContext.state
  );
  tracks.forEach(({ clipCreationIntents, clips, id: trackId }) => {
    const color = takeLaneParentsByTrackId[trackId].color;
    const generatingClips = clips
      .filter((c) => c.clipId && c.streaming)
      .map((c) => {
        return {
          studioClip: c,
          startBeats: c.startBeats,
          endBeats: c.endBeats,
          name: c.name,
        };
      });
    (
      [...clipCreationIntents, ...generatingClips] as {
        studioClip?: StudioClip;
        startBeats: number;
        endBeats: number;
        name: string;
      }[]
    ).forEach(({ startBeats, endBeats, name, studioClip }) => {
      const parentTrackId = takeLaneParentsByTrackId[trackId].id;
      const key = makeKey(parentTrackId, startBeats, endBeats);
      cacheSet.add(key);

      regions.push({
        renderAboveGL: (gl: WebGL2RenderingContext) => {
          if (
            typeof WebGL2RenderingContext === 'undefined' ||
            !(gl instanceof WebGL2RenderingContext)
          )
            throw new Error('WebGL2 context required');

          let fadeEndBeats = undefined;
          if (studioClip && studioClip.clipId) {
            // Ensure streaming buffer exists so availableDuration updates in real-time
            if (
              studioClip.streaming &&
              !prewarmedClipIds.has(studioClip.clipId)
            ) {
              prewarmedClipIds.add(studioClip.clipId);
              dspEngineSingleton()
                .then((dsp) =>
                  ensureStreamingFfmpegBufferPrewarmed(dsp, studioClip.clipId!)
                )
                .catch(() => {});
            }

            const availableDuration = studioClip.clipId
              ? getAvailableDuration(studioClip.clipId)
              : 0;
            const lastAvailableDuration = studioClip.clipId
              ? (lastAvailableDurations[studioClip.clipId] ?? 0)
              : 0;
            const displayedAvailableDuration = fInterpTo(
              lastAvailableDuration,
              availableDuration,
              0.01
            );
            lastAvailableDurations[studioClip.clipId] =
              displayedAvailableDuration;
            const displayedAvailableClipBeats = getWarpBeatsFromSeconds(
              studioClip.warp,
              displayedAvailableDuration
            );
            fadeEndBeats =
              displayedAvailableDuration === 0
                ? undefined
                : startBeats +
                  (displayedAvailableClipBeats - studioClip.readStartBeats);
          }

          const header =
            studioContext.timelineController.trackHeadersRef.current[trackId];
          if (!header) return;

          const timelineRect =
            studioContext.timelineController.getWrapperRect();
          const trackRect = getCanvasRelativeRect(
            header.getBoundingClientRect(),
            timelineRect
          );

          const resolvedEnd = endBeats ?? songEndBeats;

          // For viewport calculation, use startBeats as minimum to avoid drawing before clip start
          const viewportStartBeats = startBeats ?? songStartBeats;
          const xCSS = Math.round(
            studioContext.beatsToCanvasX(viewportStartBeats) + 1
          );
          const yCSS = Math.round(trackRect.y);
          const wCSS = Math.round(
            studioContext.beatsToCanvasX(resolvedEnd) - xCSS
          );
          const hCSS = Math.round(trackRect.height);

          // Convert to GL device px
          const canvasEl = gl.canvas as HTMLCanvasElement;
          const canvasCSSHeight = Math.round(
            canvasEl.clientHeight || canvasEl.getBoundingClientRect().height
          );
          const dpr = window.devicePixelRatio || 1;

          let vpX_css = xCSS;
          let vpY_css = canvasCSSHeight - (yCSS + hCSS);
          let vpW_css = wCSS;
          let vpH_css = hCSS;

          if (vpX_css < 0) {
            vpW_css += vpX_css;
            vpX_css = 0;
          }
          if (vpY_css < 0) {
            vpH_css += vpY_css;
            vpY_css = 0;
          }

          const vpX = Math.max(0, Math.round(vpX_css * dpr));
          const vpY = Math.max(0, Math.round(vpY_css * dpr));
          let vpW = Math.max(0, Math.round(vpW_css * dpr));
          let vpH = Math.max(0, Math.round(vpH_css * dpr));

          const maxDims = gl.getParameter(gl.MAX_VIEWPORT_DIMS) as [
            number,
            number,
          ];
          vpW = Math.min(vpW, maxDims[0] - vpX);
          vpH = Math.min(vpH, maxDims[1] - vpY);
          if (vpW <= 0 || vpH <= 0) return;

          const three = getOrCreateThree(gl);
          let rr = webGLCache.renderers.get(key) as StripesRegion | undefined;
          if (!rr) {
            const seed = seededNumber(`${parentTrackId}:${name ?? ''}`);
            rr = new StripesRegion(
              three,
              seed,
              getStripePalette(studioClip?.color || color)
            );
            rr.enableSpotlight(SPOTLIGHT.enabled);
            rr.setRedrawCallback(() => {
              studioContext.timelineController.frameCountRef.current++;
            });
            webGLCache.renderers.set(key, rr);
          }

          rr.assignStripePalette(getStripePalette(studioClip?.color || color));
          rr.setAnchorBeats(startBeats ?? songStartBeats ?? 0);

          // Expand viewport for shader edges
          three.setScissorTest(true);
          const expand = Math.max(AA_EXPAND_DEVICE_PX, Math.ceil(dpr));
          const ex = Math.max(0, vpX - expand);
          const ey = Math.max(0, vpY - expand);
          let ew = vpW + expand * 2;
          let eh = vpH + expand * 2;
          if (ex + ew > maxDims[0]) ew = Math.max(0, maxDims[0] - ex);
          if (ey + eh > maxDims[1]) eh = Math.max(0, maxDims[1] - ey);
          // Expand scissor, but keep the viewport exact
          three.setScissor(ex, ey, ew, eh);
          three.setViewport(vpX, vpY, vpW, vpH);

          // Corner radius (true px)
          const radiusPx = Math.min(
            clipCornerRadius * dpr,
            Math.min(vpW, vpH) * 0.5
          );

          // Use the REAL timeline zoom pivot (mouse) if available; raw (unclamped) device px.
          const mousePos = ENABLE_MOUSE
            ? studioContext.timelineController.canvasRelativeMousePositionRef
                .current
            : null;
          const mx = mousePos?.x ?? vpX_css + vpW_css * 0.5;
          const my = mousePos?.y ?? yCSS + hCSS * 0.5;
          const localMouse = {
            x: (mx - vpX_css) * dpr, // can be <0 or >vpW -> "outside" -> no phase pin
            y: (my - yCSS) * dpr,
          };

          // Calculate timeline position at viewport's left edge for pattern anchoring
          // vpX is in device pixels, convert to CSS pixels, then to timeline beats
          const pxPerBeat =
            studioContext.timelineController.pxPerBeatRef.current;
          const scrollX = studioContext.timelineController.scrollXRef.current;
          const viewportLeftCSS = vpX / dpr;
          const timelineBeatsAtViewportLeft =
            (viewportLeftCSS + scrollX) / pxPerBeat;

          rr.setViewport(
            vpW,
            vpH,
            radiusPx,
            localMouse,
            { x: vpX, y: vpY },
            timelineBeatsAtViewportLeft,
            pxPerBeat,
            dpr
          );
          // Calculate fade positions in device pixels relative to viewport
          const fadeEndCanvasPx = fadeEndBeats
            ? studioContext.beatsToCanvasX(fadeEndBeats)
            : 0;

          // Convert to device pixels and make relative to viewport
          const fadeEndPx = fadeEndCanvasPx * dpr - vpX;

          rr.setFadeStart(fadeEndBeats !== undefined, 60.0, fadeEndPx);
          rr.render();

          three.setScissorTest(false);
          three.resetState();
        },
      });
    });
  });

  // Cleanup stale renderers
  for (const key of webGLCache.renderers.keys()) {
    if (!cacheSet.has(key)) {
      const r = webGLCache.renderers.get(key);
      if (r && typeof (r as any).dispose === 'function') {
        try {
          (r as any).dispose();
        } catch {}
      }
      webGLCache.renderers.delete(key);
    }
  }

  return regions;
}
