import Phaser from "phaser";

const LIGHT_MAX_SEGMENTS = 16;
const LIGHT_MAX_BOXES = 48;
const LIGHT_MAX_CIRCLES = 24;
const LIGHT_MAX_SAMPLES = 6;
const TEMPORAL_HISTORY_FRAMES = 5;

const LIGHTING_FRAG = `
precision highp float;
precision mediump int;

#define MAX_SEGMENTS ${LIGHT_MAX_SEGMENTS}
#define MAX_BOXES ${LIGHT_MAX_BOXES}
#define MAX_CIRCLES ${LIGHT_MAX_CIRCLES}
#define MAX_SAMPLES ${LIGHT_MAX_SAMPLES}

uniform vec2 uSceneSize;
uniform float uFrameSeed;
uniform vec2 uCameraOffset;
uniform vec4 uSceneRect;
uniform float uLightVisibility;

uniform int uSegmentCount;
uniform vec4 uLightSegments[MAX_SEGMENTS];
uniform vec3 uLightColors[MAX_SEGMENTS];

uniform int uBoxCount;
uniform vec4 uShadowBoxes[MAX_BOXES];

uniform int uCircleCount;
uniform vec3 uShadowCircles[MAX_CIRCLES];

uniform int uSampleCount;
uniform float uLightIntensity;
uniform float uMinDistanceSq;
uniform float uMaxContribution;

varying vec2 outTexCoord;

float ign(vec2 p, float seed) {
  return fract(52.9829189 * fract(0.06711056 * p.x + 0.00583715 * p.y + 0.000183 * seed));
}

vec2 sampleR2(float i) {
  const float a1 = 0.7548776662466927;
  const float a2 = 0.5698402909980532;
  return fract(vec2(0.5 + a1 * i, 0.5 + a2 * i));
}

vec2 cranleyRotation(vec2 pixel, float seed) {
  float r1 = ign(pixel + vec2(17.13, 43.71), seed * 1.13 + 19.0);
  float r2 = ign(pixel + vec2(89.31, 11.17), seed * 0.91 + 53.0);
  return vec2(r1, r2);
}

bool pointInBox(vec2 point, vec4 box) {
  return point.x >= box.x && point.x <= box.z && point.y >= box.y && point.y <= box.w;
}

bool pointInCircle(vec2 point, vec3 circle) {
  vec2 d = point - circle.xy;
  return dot(d, d) < circle.z * circle.z;
}

bool pointInsideGeometry(vec2 point) {
  for (int i = 0; i < MAX_BOXES; i += 1) {
    if (i >= uBoxCount) {
      break;
    }
    if (pointInBox(point, uShadowBoxes[i])) {
      return true;
    }
  }

  for (int i = 0; i < MAX_CIRCLES; i += 1) {
    if (i >= uCircleCount) {
      break;
    }
    if (pointInCircle(point, uShadowCircles[i])) {
      return true;
    }
  }

  return false;
}

bool rayHitsAabb(vec2 origin, vec2 dir, float maxDist, vec4 box) {
  float tMin = 0.0;
  float tMax = maxDist;

  if (abs(dir.x) < 0.00001) {
    if (origin.x <= box.x || origin.x >= box.z) {
      return false;
    }
  } else {
    float tx1 = (box.x - origin.x) / dir.x;
    float tx2 = (box.z - origin.x) / dir.x;
    float txMin = min(tx1, tx2);
    float txMax = max(tx1, tx2);
    tMin = max(tMin, txMin);
    tMax = min(tMax, txMax);
  }

  if (abs(dir.y) < 0.00001) {
    if (origin.y <= box.y || origin.y >= box.w) {
      return false;
    }
  } else {
    float ty1 = (box.y - origin.y) / dir.y;
    float ty2 = (box.w - origin.y) / dir.y;
    float tyMin = min(ty1, ty2);
    float tyMax = max(ty1, ty2);
    tMin = max(tMin, tyMin);
    tMax = min(tMax, tyMax);
  }

  if (tMax < tMin) {
    return false;
  }

  float endMargin = 1.2;
  return tMin > 0.0005 && tMin < (maxDist - endMargin);
}

bool rayHitsCircle(vec2 origin, vec2 dir, float maxDist, vec3 circle) {
  vec2 m = origin - circle.xy;
  float radius = circle.z;
  float b = dot(m, dir);
  float c = dot(m, m) - radius * radius;

  if (c <= 0.0) {
    return true;
  }

  float disc = b * b - c;
  if (disc < 0.0) {
    return false;
  }

  float sqrtDisc = sqrt(disc);
  float t = -b - sqrtDisc;
  if (t < 0.0005) {
    t = -b + sqrtDisc;
  }

  float endMargin = 1.2;
  return t > 0.0005 && t < (maxDist - endMargin);
}

bool traceVisibility(vec2 start, vec2 end) {
  if (pointInsideGeometry(start) || pointInsideGeometry(end)) {
    return false;
  }

  vec2 delta = end - start;
  float dist = length(delta);
  if (dist < 0.001) {
    return false;
  }

  vec2 dir = delta / dist;

  for (int i = 0; i < MAX_BOXES; i += 1) {
    if (i >= uBoxCount) {
      break;
    }
    if (rayHitsAabb(start, dir, dist, uShadowBoxes[i])) {
      return false;
    }
  }

  for (int i = 0; i < MAX_CIRCLES; i += 1) {
    if (i >= uCircleCount) {
      break;
    }
    if (rayHitsCircle(start, dir, dist, uShadowCircles[i])) {
      return false;
    }
  }

  return true;
}

vec4 segmentAt(int targetIndex) {
  vec4 segment = vec4(0.0);
  for (int i = 0; i < MAX_SEGMENTS; i += 1) {
    if (i == targetIndex) {
      segment = uLightSegments[i];
    }
  }
  return segment;
}

vec3 segmentColorAt(int targetIndex) {
  vec3 color = vec3(0.0);
  for (int i = 0; i < MAX_SEGMENTS; i += 1) {
    if (i == targetIndex) {
      color = uLightColors[i];
    }
  }
  return color;
}

vec3 computeMonteCarloLight(vec2 pixelPos) {
  if (uSegmentCount <= 0 || uSampleCount <= 0) {
    return vec3(0.0);
  }

  vec3 accum = vec3(0.0);
  float sampleCount = float(uSampleCount);
  float segmentCountF = float(uSegmentCount);
  vec2 pixel = floor(pixelPos) + vec2(0.5);
  vec2 cpRotation = cranleyRotation(pixel, uFrameSeed);

  for (int s = 0; s < MAX_SAMPLES; s += 1) {
    if (s >= uSampleCount) {
      break;
    }

    vec2 xi = fract(sampleR2(float(s)) + cpRotation);
    float segIndexF = floor(xi.x * segmentCountF);
    segIndexF = clamp(segIndexF, 0.0, float(MAX_SEGMENTS - 1));
    int segIndex = int(segIndexF);

    vec4 segment = segmentAt(segIndex);
    vec3 lightColor = segmentColorAt(segIndex);
    vec2 segmentVec = segment.zw - segment.xy;
    float segmentLen = length(segmentVec);
    if (segmentLen < 0.001) {
      continue;
    }

    float tRnd = xi.y;
    vec2 lightPoint = mix(segment.xy, segment.zw, tRnd);

    if (!traceVisibility(pixelPos, lightPoint)) {
      continue;
    }

    vec2 toLight = lightPoint - pixelPos;
    float minDist = sqrt(max(uMinDistanceSq, 0.00001));
    float dist = max(length(toLight), minDist);
    float pdf = 1.0 / (segmentCountF * segmentLen);

    // Keep total power per segment bounded so long segments don't flatten the whole scene.
    float linePowerDensity = 1.0 / max(segmentLen, 0.00001);
    float contributionScale = ((1.0 / dist) * linePowerDensity) / max(pdf, 0.000001);
    vec3 rawContribution = lightColor * uLightIntensity * contributionScale;
    vec3 contribution = rawContribution / (vec3(1.0) + rawContribution / vec3(uMaxContribution));

    accum += contribution;
  }

  return accum / max(sampleCount, 1.0);
}

void main() {
  vec2 pixelPos = vec2(
    outTexCoord.x * uSceneSize.x,
    (1.0 - outTexCoord.y) * uSceneSize.y
  );
  pixelPos -= uCameraOffset;

  bool insideScene = pixelPos.x >= uSceneRect.x && pixelPos.x <= uSceneRect.z
    && pixelPos.y >= uSceneRect.y && pixelPos.y <= uSceneRect.w;

  vec3 lighting = vec3(0.0);
  if (insideScene && uLightVisibility > 0.001) {
    lighting = computeMonteCarloLight(pixelPos) * uLightVisibility;
  }
  gl_FragColor = vec4(max(lighting, vec3(0.0)), 1.0);
}
`;

const COPY_FRAG = `
precision mediump float;

uniform sampler2D uMainSampler;
varying vec2 outTexCoord;

void main() {
  gl_FragColor = texture2D(uMainSampler, outTexCoord);
}
`;

const TEMPORAL_ACCUM_FRAG = `
precision mediump float;
precision mediump int;

uniform sampler2D uMainSampler;
uniform sampler2D uHistory1;
uniform sampler2D uHistory2;
uniform sampler2D uHistory3;
uniform sampler2D uHistory4;
uniform int uTemporalCount;
uniform float uTemporalDisocclusionThreshold;
varying vec2 outTexCoord;

void main() {
  vec3 current = texture2D(uMainSampler, outTexCoord).rgb;
  float currentLum = dot(current, vec3(0.2126, 0.7152, 0.0722));

  vec3 accum = current;
  float total = 1.0;

  if (uTemporalCount > 1) {
    vec3 h1 = texture2D(uHistory1, outTexCoord).rgb;
    float keep1 = step(dot(h1, vec3(0.2126, 0.7152, 0.0722)), currentLum + uTemporalDisocclusionThreshold);
    accum += h1 * keep1;
    total += keep1;
  }
  if (uTemporalCount > 2) {
    vec3 h2 = texture2D(uHistory2, outTexCoord).rgb;
    float keep2 = step(dot(h2, vec3(0.2126, 0.7152, 0.0722)), currentLum + uTemporalDisocclusionThreshold);
    accum += h2 * keep2;
    total += keep2;
  }
  if (uTemporalCount > 3) {
    vec3 h3 = texture2D(uHistory3, outTexCoord).rgb;
    float keep3 = step(dot(h3, vec3(0.2126, 0.7152, 0.0722)), currentLum + uTemporalDisocclusionThreshold);
    accum += h3 * keep3;
    total += keep3;
  }
  if (uTemporalCount > 4) {
    vec3 h4 = texture2D(uHistory4, outTexCoord).rgb;
    float keep4 = step(dot(h4, vec3(0.2126, 0.7152, 0.0722)), currentLum + uTemporalDisocclusionThreshold);
    accum += h4 * keep4;
    total += keep4;
  }

  gl_FragColor = vec4(accum / max(total, 0.00001), 1.0);
}
`;

const DENOISE_FRAG = `
precision highp float;

uniform sampler2D uMainSampler;
uniform sampler2D uGuideSampler;
uniform vec2 uTexelSize;
uniform float uDenoiseSpatialInvSigma2;
uniform float uDenoiseColorInvSigma2;
uniform float uDenoiseRadiusSq;
uniform float uDenoiseBlend;
varying vec2 outTexCoord;

vec3 toLinear(vec3 srgb) {
  return pow(max(srgb, vec3(0.0)), vec3(2.2));
}

float weightFor(vec2 offset, vec3 guideCenter, vec3 guideSample) {
  float spatialW = exp(-dot(offset, offset) * uDenoiseSpatialInvSigma2);
  vec3 diff = guideSample - guideCenter;
  float colorW = exp(-dot(diff, diff) * uDenoiseColorInvSigma2);
  return spatialW * colorW;
}

void main() {
  vec3 centerLight = texture2D(uMainSampler, outTexCoord).rgb;
  vec3 centerGuide = toLinear(texture2D(uGuideSampler, outTexCoord).rgb);

  vec3 accum = centerLight;
  float total = 1.0;

  for (int jy = 0; jy < 5; jy += 1) {
    for (int ix = 0; ix < 5; ix += 1) {
      if (ix == 2 && jy == 2) {
        continue;
      }

      vec2 off = vec2(float(ix - 2), float(jy - 2));
      if (dot(off, off) > uDenoiseRadiusSq) {
        continue;
      }

      vec2 uv = outTexCoord + off * uTexelSize;
      vec3 sampleLight = texture2D(uMainSampler, uv).rgb;
      vec3 sampleGuide = toLinear(texture2D(uGuideSampler, uv).rgb);
      float w = weightFor(off, centerGuide, sampleGuide);
      accum += sampleLight * w;
      total += w;
    }
  }

  vec3 filtered = accum / max(total, 0.00001);
  vec3 result = mix(centerLight, filtered, uDenoiseBlend);
  gl_FragColor = vec4(result, 1.0);
}
`;

const PREFILTER_FRAG = `
precision mediump float;

uniform sampler2D uMainSampler;
uniform float uBloomInputGain;
uniform float uBloomCompression;
uniform float uSceneBloomFloor;
uniform float uSceneBloomBias;
varying vec2 outTexCoord;

void main() {
  vec3 srgb = texture2D(uMainSampler, outTexCoord).rgb;
  vec3 color = pow(max(srgb, vec3(0.0)), vec3(2.2));
  float luminance = dot(color, vec3(0.2126, 0.7152, 0.0722));
  float sceneWeight = uSceneBloomFloor + (1.0 - uSceneBloomFloor) * pow(clamp(luminance, 0.0, 1.0), uSceneBloomBias);
  vec3 bloom = color * uBloomInputGain * sceneWeight;
  float bloomLum = dot(bloom, vec3(0.2126, 0.7152, 0.0722));
  float compression = 1.0 / (1.0 + bloomLum * uBloomCompression);
  gl_FragColor = vec4(bloom * compression, 1.0);
}
`;

const DOWNSAMPLE_FRAG = `
precision mediump float;

uniform sampler2D uMainSampler;
uniform vec2 uTexelSize;
uniform float uRadius;
varying vec2 outTexCoord;

vec3 tap(vec2 offset) {
  return texture2D(uMainSampler, outTexCoord + offset * uTexelSize * uRadius).rgb;
}

void main() {
  vec3 accum = vec3(0.0);
  float total = 0.0;

  float wCenter = 0.11;
  float wNearCross = 0.10;
  float wNearDiag = 0.08;
  float wFarCross = 0.06;
  float wFarDiag = 0.045;

  accum += tap(vec2(0.0, 0.0)) * wCenter;
  total += wCenter;

  accum += tap(vec2(-1.0, 0.0)) * wNearCross;
  accum += tap(vec2(1.0, 0.0)) * wNearCross;
  accum += tap(vec2(0.0, -1.0)) * wNearCross;
  accum += tap(vec2(0.0, 1.0)) * wNearCross;
  total += 4.0 * wNearCross;

  accum += tap(vec2(-1.0, -1.0)) * wNearDiag;
  accum += tap(vec2(1.0, -1.0)) * wNearDiag;
  accum += tap(vec2(-1.0, 1.0)) * wNearDiag;
  accum += tap(vec2(1.0, 1.0)) * wNearDiag;
  total += 4.0 * wNearDiag;

  accum += tap(vec2(-2.0, 0.0)) * wFarCross;
  accum += tap(vec2(2.0, 0.0)) * wFarCross;
  accum += tap(vec2(0.0, -2.0)) * wFarCross;
  accum += tap(vec2(0.0, 2.0)) * wFarCross;
  total += 4.0 * wFarCross;

  accum += tap(vec2(-2.0, -2.0)) * wFarDiag;
  accum += tap(vec2(2.0, -2.0)) * wFarDiag;
  accum += tap(vec2(-2.0, 2.0)) * wFarDiag;
  accum += tap(vec2(2.0, 2.0)) * wFarDiag;
  total += 4.0 * wFarDiag;

  gl_FragColor = vec4(accum / max(total, 0.00001), 1.0);
}
`;

const UPSAMPLE_FRAG = `
precision mediump float;

uniform sampler2D uMainSampler;
uniform sampler2D uLowSampler;
uniform vec2 uLowTexelSize;
uniform float uRadius;
uniform float uLowWeight;
varying vec2 outTexCoord;

vec3 tent9(sampler2D tex, vec2 uv, vec2 texelSize) {
  vec3 c = vec3(0.0);
  c += texture2D(tex, uv + texelSize * vec2(-1.0, -1.0)).rgb;
  c += texture2D(tex, uv + texelSize * vec2(0.0, -1.0)).rgb * 2.0;
  c += texture2D(tex, uv + texelSize * vec2(1.0, -1.0)).rgb;
  c += texture2D(tex, uv + texelSize * vec2(-1.0, 0.0)).rgb * 2.0;
  c += texture2D(tex, uv).rgb * 4.0;
  c += texture2D(tex, uv + texelSize * vec2(1.0, 0.0)).rgb * 2.0;
  c += texture2D(tex, uv + texelSize * vec2(-1.0, 1.0)).rgb;
  c += texture2D(tex, uv + texelSize * vec2(0.0, 1.0)).rgb * 2.0;
  c += texture2D(tex, uv + texelSize * vec2(1.0, 1.0)).rgb;
  return c / 16.0;
}

void main() {
  vec3 high = texture2D(uMainSampler, outTexCoord).rgb;
  vec3 low = tent9(uLowSampler, outTexCoord, uLowTexelSize * uRadius);
  gl_FragColor = vec4(high + low * uLowWeight, 1.0);
}
`;

const COMPOSITE_FRAG = `
precision mediump float;

uniform sampler2D uMainSampler;
uniform sampler2D uBloomSampler;
uniform sampler2D uLightSampler;
uniform float uBloomStrength;
uniform float uExposure;
varying vec2 outTexCoord;

void main() {
  vec4 sceneTex = texture2D(uMainSampler, outTexCoord);
  vec3 sceneLinear = pow(max(sceneTex.rgb, vec3(0.0)), vec3(2.2));
  vec3 lightingLinear = texture2D(uLightSampler, outTexCoord).rgb;
  vec3 bloomLinear = texture2D(uBloomSampler, outTexCoord).rgb;

  vec3 litScene = sceneLinear + lightingLinear;
  vec3 hdr = litScene * uExposure + bloomLinear * uBloomStrength;

  vec3 mapped = vec3(1.0) - exp(-hdr);
  vec3 gammaCorrect = pow(max(mapped, vec3(0.0)), vec3(1.0 / 2.2));

  gl_FragColor = vec4(gammaCorrect, sceneTex.a);
}
`;

type GL = WebGLRenderingContext | WebGL2RenderingContext;
type WebGLRenderer = Phaser.Renderer.WebGL.WebGLRenderer;
type RenderTargetLike = Phaser.Renderer.WebGL.RenderTarget;
type WebGLShader = Phaser.Renderer.WebGL.WebGLShader;

interface LightingData {
  segments: ArrayLike<number>;
  segmentColors: ArrayLike<number>;
  boxes: ArrayLike<number>;
  circles: ArrayLike<number>;
  sceneRect?: readonly [number, number, number, number];
  sampleCount?: number;
  intensity?: number;
  minDistanceSq?: number;
  maxContribution?: number;
  frameSeed?: number;
}

interface HdrTextureConfig {
  internalFormat: number;
  format: number;
  type: number;
  label: string;
}

class HdrRenderTarget {
  width = 1;

  height = 1;

  autoClear = true;

  texture: { webGLTexture: WebGLTexture } = { webGLTexture: null as unknown as WebGLTexture };

  framebuffer: { webGLFramebuffer: WebGLFramebuffer; renderTexture: { width: number; height: number } } = {
    webGLFramebuffer: null as unknown as WebGLFramebuffer,
    renderTexture: { width: 1, height: 1 }
  };

  constructor(
    private readonly renderer: WebGLRenderer,
    private readonly gl: GL,
    private readonly hdr: HdrTextureConfig,
    readonly scale: number
  ) {
    this.resize(renderer.width, renderer.height);
  }

  resize(baseWidth: number, baseHeight: number): void {
    const width = Math.max(1, Math.floor(baseWidth * this.scale));
    const height = Math.max(1, Math.floor(baseHeight * this.scale));

    if (this.width === width && this.height === height && this.texture.webGLTexture && this.framebuffer.webGLFramebuffer) {
      return;
    }

    this.destroy();

    const gl = this.gl;
    const texture = gl.createTexture();
    const framebuffer = gl.createFramebuffer();

    if (!texture || !framebuffer) {
      throw new Error("Failed to create HDR bloom resources.");
    }

    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

    if (typeof WebGL2RenderingContext !== "undefined" && gl instanceof WebGL2RenderingContext) {
      gl.texImage2D(
        gl.TEXTURE_2D,
        0,
        this.hdr.internalFormat,
        width,
        height,
        0,
        this.hdr.format,
        this.hdr.type,
        null
      );
    } else {
      gl.texImage2D(
        gl.TEXTURE_2D,
        0,
        this.hdr.internalFormat,
        width,
        height,
        0,
        this.hdr.format,
        this.hdr.type,
        null
      );
    }

    gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);

    const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);

    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    gl.bindTexture(gl.TEXTURE_2D, null);

    if (status !== gl.FRAMEBUFFER_COMPLETE) {
      gl.deleteFramebuffer(framebuffer);
      gl.deleteTexture(texture);
      throw new Error(`HDR framebuffer incomplete (${status}).`);
    }

    this.width = width;
    this.height = height;
    this.texture = { webGLTexture: texture };
    this.framebuffer = {
      webGLFramebuffer: framebuffer,
      renderTexture: { width, height }
    };
  }

  bind(adjustViewport = false, width?: number, height?: number): void {
    if (typeof width === "number" && typeof height === "number") {
      this.resize(width, height);
    }

    if (adjustViewport) {
      this.renderer.flush();
    }

    this.renderer.pushFramebuffer(this.framebuffer as unknown as Phaser.Renderer.WebGL.Wrappers.WebGLFramebufferWrapper, false, false);

    if (adjustViewport) {
      this.gl.viewport(0, 0, this.width, this.height);
      this.gl.disable(this.gl.SCISSOR_TEST);
    }

    if (this.autoClear) {
      this.gl.clearColor(0, 0, 0, 0);
      this.gl.clear(this.gl.COLOR_BUFFER_BIT);
    }

    this.renderer.clearStencilMask();
  }

  destroy(): void {
    const gl = this.gl;
    if (this.framebuffer.webGLFramebuffer) {
      gl.deleteFramebuffer(this.framebuffer.webGLFramebuffer);
    }
    if (this.texture.webGLTexture) {
      gl.deleteTexture(this.texture.webGLTexture);
    }

    this.texture = { webGLTexture: null as unknown as WebGLTexture };
    this.framebuffer = {
      webGLFramebuffer: null as unknown as WebGLFramebuffer,
      renderTexture: { width: 1, height: 1 }
    };
  }
}

export class CodBloomPipeline extends Phaser.Renderer.WebGL.Pipelines.PostFXPipeline {
  static readonly KEY = "CodBloom";

  bloomStrength = 0.2;

  bloomRadius = 1.24;

  hdrExposure = 1.4;

  bloomInputGain = 1.2;

  bloomCompression = 0.7;

  sceneBloomFloor = 0.06;

  sceneBloomBias = 1.15;

  upsampleWeight = 1.05;

  private captureTarget: HdrRenderTarget | null = null;

  private lightingTarget: HdrRenderTarget | null = null;

  private temporalTarget: HdrRenderTarget | null = null;

  private temporalHistoryTargets: HdrRenderTarget[] = [];

  private denoiseTarget: HdrRenderTarget | null = null;

  private bloomTargets: HdrRenderTarget[] = [];

  private hdrReady = false;

  private warned = false;

  private lightSegments = new Float32Array(LIGHT_MAX_SEGMENTS * 4);

  private lightColors = new Float32Array(LIGHT_MAX_SEGMENTS * 3);

  private shadowBoxes = new Float32Array(LIGHT_MAX_BOXES * 4);

  private shadowCircles = new Float32Array(LIGHT_MAX_CIRCLES * 3);

  private segmentCount = 0;

  private boxCount = 0;

  private circleCount = 0;

  private sampleCount = 12;

  private lightIntensity = 4.0;

  private minDistanceSq = 8 * 8;

  private maxContribution = 1.0;

  private frameSeed = 1;

  private seedBase = 1;

  private seedCycleStep = 73.17;

  private temporalPhase = 0;

  private temporalCount = 0;

  private cameraOffsetX = 0;

  private cameraOffsetY = 0;

  private lightVisibility = 1;

  private lightingEnabled = true;

  private sceneRect = new Float32Array([0, 0, 1, 1]);

  private denoiseSpatialInvSigma2 = 0.24;

  private denoiseColorInvSigma2 = 18.0;

  private denoiseRadiusSq = 2.25;

  private denoiseBlend = 0.88;

  private temporalDisocclusionThreshold = 0.035;

  constructor(game: Phaser.Game) {
    super({
      game,
      renderTarget: 1,
      shaders: [
        { name: "Lighting", fragShader: LIGHTING_FRAG },
        { name: "Copy", fragShader: COPY_FRAG },
        { name: "TemporalAccum", fragShader: TEMPORAL_ACCUM_FRAG },
        { name: "Denoise", fragShader: DENOISE_FRAG },
        { name: "Prefilter", fragShader: PREFILTER_FRAG },
        { name: "Downsample", fragShader: DOWNSAMPLE_FRAG },
        { name: "Upsample", fragShader: UPSAMPLE_FRAG },
        { name: "Composite", fragShader: COMPOSITE_FRAG }
      ]
    });
  }

  bootFX(): void {
    super.bootFX();
    this.hdrReady = this.setupHdrTargets();

    if (this.hdrReady && this.captureTarget) {
      this.currentRenderTarget = this.captureTarget as unknown as RenderTargetLike;
    }
  }

  onResize(width: number, height: number): void {
    if (!this.hdrReady) {
      return;
    }

    this.captureTarget?.resize(width, height);
    this.lightingTarget?.resize(width, height);
    this.temporalTarget?.resize(width, height);
    for (const target of this.temporalHistoryTargets) {
      target.resize(width, height);
    }
    this.denoiseTarget?.resize(width, height);
    for (const target of this.bloomTargets) {
      target.resize(width, height);
    }
  }

  setLightingData(data: LightingData): void {
    const segmentItemCount = Math.min(LIGHT_MAX_SEGMENTS, Math.floor(data.segments.length / 4));
    const colorItemCount = Math.floor(data.segmentColors.length / 3);

    this.lightSegments.fill(0);
    this.lightColors.fill(0);

    for (let i = 0; i < segmentItemCount; i += 1) {
      const segOffset = i * 4;
      this.lightSegments[segOffset + 0] = Number(data.segments[segOffset + 0] ?? 0);
      this.lightSegments[segOffset + 1] = Number(data.segments[segOffset + 1] ?? 0);
      this.lightSegments[segOffset + 2] = Number(data.segments[segOffset + 2] ?? 0);
      this.lightSegments[segOffset + 3] = Number(data.segments[segOffset + 3] ?? 0);

      const colorOffset = i * 3;
      if (i < colorItemCount) {
        this.lightColors[colorOffset + 0] = Number(data.segmentColors[colorOffset + 0] ?? 1);
        this.lightColors[colorOffset + 1] = Number(data.segmentColors[colorOffset + 1] ?? 1);
        this.lightColors[colorOffset + 2] = Number(data.segmentColors[colorOffset + 2] ?? 1);
      } else {
        this.lightColors[colorOffset + 0] = 1;
        this.lightColors[colorOffset + 1] = 1;
        this.lightColors[colorOffset + 2] = 1;
      }
    }
    this.segmentCount = segmentItemCount;

    this.shadowBoxes.fill(0);
    this.boxCount = Math.min(LIGHT_MAX_BOXES, Math.floor(data.boxes.length / 4));
    for (let i = 0; i < this.boxCount; i += 1) {
      const offset = i * 4;
      this.shadowBoxes[offset + 0] = Number(data.boxes[offset + 0] ?? 0);
      this.shadowBoxes[offset + 1] = Number(data.boxes[offset + 1] ?? 0);
      this.shadowBoxes[offset + 2] = Number(data.boxes[offset + 2] ?? 0);
      this.shadowBoxes[offset + 3] = Number(data.boxes[offset + 3] ?? 0);
    }

    this.shadowCircles.fill(0);
    this.circleCount = Math.min(LIGHT_MAX_CIRCLES, Math.floor(data.circles.length / 3));
    for (let i = 0; i < this.circleCount; i += 1) {
      const offset = i * 3;
      this.shadowCircles[offset + 0] = Number(data.circles[offset + 0] ?? 0);
      this.shadowCircles[offset + 1] = Number(data.circles[offset + 1] ?? 0);
      this.shadowCircles[offset + 2] = Number(data.circles[offset + 2] ?? 0);
    }

    if (typeof data.sampleCount === "number" && Number.isFinite(data.sampleCount)) {
      this.sampleCount = Phaser.Math.Clamp(Math.floor(data.sampleCount), 1, LIGHT_MAX_SAMPLES);
    }
    if (typeof data.intensity === "number" && Number.isFinite(data.intensity)) {
      this.lightIntensity = Math.max(0, data.intensity);
    }
    if (typeof data.minDistanceSq === "number" && Number.isFinite(data.minDistanceSq)) {
      this.minDistanceSq = Math.max(1e-4, data.minDistanceSq);
    }
    if (typeof data.maxContribution === "number" && Number.isFinite(data.maxContribution)) {
      this.maxContribution = Math.max(1e-5, data.maxContribution);
    }
    if (typeof data.frameSeed === "number" && Number.isFinite(data.frameSeed)) {
      this.seedBase = data.frameSeed;
    }
    if (data.sceneRect) {
      this.sceneRect[0] = Number(data.sceneRect[0]);
      this.sceneRect[1] = Number(data.sceneRect[1]);
      this.sceneRect[2] = Number(data.sceneRect[2]);
      this.sceneRect[3] = Number(data.sceneRect[3]);
    }
  }

  setLightingView(cameraOffsetX: number, cameraOffsetY: number, visibility: number): void {
    this.cameraOffsetX = Number.isFinite(cameraOffsetX) ? cameraOffsetX : 0;
    this.cameraOffsetY = Number.isFinite(cameraOffsetY) ? cameraOffsetY : 0;
    this.lightVisibility = Number.isFinite(visibility) ? Phaser.Math.Clamp(visibility, 0, 1) : 1;
  }

  setLightingEnabled(enabled: boolean): void {
    const normalized = Boolean(enabled);
    if (normalized === this.lightingEnabled) {
      return;
    }
    this.lightingEnabled = normalized;
    this.resetTemporalHistory();
  }

  resetTemporalHistory(): void {
    this.temporalPhase = 0;
    this.temporalCount = 0;
  }

  onDraw(source: RenderTargetLike): void {
    if (!this.hdrReady) {
      this.copyToGame(source);
      return;
    }

    const capture = this.captureTarget;
    const lighting = this.lightingTarget;
    const temporal = this.temporalTarget;
    const denoised = this.denoiseTarget;
    if (!capture || !lighting || !temporal || !denoised || this.bloomTargets.length < 7 || this.temporalHistoryTargets.length < TEMPORAL_HISTORY_FRAMES) {
      this.copyToGame(source);
      return;
    }

    const half = this.bloomTargets[0];
    const quarter = this.bloomTargets[1];
    const eighth = this.bloomTargets[2];
    const sixteenth = this.bloomTargets[3];
    const upEighth = this.bloomTargets[4];
    const upQuarter = this.bloomTargets[5];
    const upHalf = this.bloomTargets[6];

    if (this.lightingEnabled) {
      this.frameSeed = this.seedBase + this.temporalPhase * this.seedCycleStep;
      this.runLighting(capture, lighting);
      this.runCopy(lighting, this.temporalHistoryTargets[this.temporalPhase]);
      this.temporalCount = Math.min(TEMPORAL_HISTORY_FRAMES, this.temporalCount + 1);
      this.runTemporalAccum(temporal, this.temporalCount, this.temporalPhase);
      this.temporalPhase = (this.temporalPhase + 1) % TEMPORAL_HISTORY_FRAMES;

      this.runDenoise(temporal, capture, denoised);
    } else {
      this.temporalCount = 0;
      this.temporalPhase = 0;
      this.clearRenderTarget(denoised);
    }

    // Bloom should be built from the captured scene, while denoised lighting is composited separately.
    this.runPrefilter(capture, half);

    this.runDownsample(half, quarter);
    this.runDownsample(quarter, eighth);
    this.runDownsample(eighth, sixteenth);

    this.runUpsample(eighth, sixteenth, upEighth);
    this.runUpsample(quarter, upEighth, upQuarter);
    this.runUpsample(half, upQuarter, upHalf);

    this.runComposite(capture, upHalf, denoised);
  }

  destroy(): this {
    this.destroyHdrTargets();
    return super.destroy();
  }

  private setupHdrTargets(): boolean {
    this.destroyHdrTargets();

    const hdr = this.resolveHdrMode();
    if (!hdr) {
      if (!this.warned) {
        this.warned = true;
        console.warn("CodBloom: HDR + linear filtering is unavailable on this GPU/context.");
      }
      return false;
    }

    try {
      this.captureTarget = new HdrRenderTarget(this.renderer as WebGLRenderer, this.gl as GL, hdr, 1);
      this.captureTarget.autoClear = true;

      this.lightingTarget = new HdrRenderTarget(this.renderer as WebGLRenderer, this.gl as GL, hdr, 1);
      this.lightingTarget.autoClear = true;

      this.temporalTarget = new HdrRenderTarget(this.renderer as WebGLRenderer, this.gl as GL, hdr, 1);
      this.temporalTarget.autoClear = true;

      this.temporalHistoryTargets = Array.from({ length: TEMPORAL_HISTORY_FRAMES }, () => {
        const target = new HdrRenderTarget(this.renderer as WebGLRenderer, this.gl as GL, hdr, 1);
        target.autoClear = true;
        return target;
      });

      this.denoiseTarget = new HdrRenderTarget(this.renderer as WebGLRenderer, this.gl as GL, hdr, 1);
      this.denoiseTarget.autoClear = true;

      const scales = [0.5, 0.25, 0.125, 0.0625, 0.125, 0.25, 0.5];
      this.bloomTargets = scales.map((scale) => {
        const target = new HdrRenderTarget(this.renderer as WebGLRenderer, this.gl as GL, hdr, scale);
        target.autoClear = true;
        return target;
      });
    } catch (error) {
      this.destroyHdrTargets();
      if (!this.warned) {
        this.warned = true;
        console.warn("CodBloom: failed to initialize HDR targets", error);
      }
      return false;
    }

    return true;
  }

  private destroyHdrTargets(): void {
    for (const target of this.bloomTargets) {
      target.destroy();
    }
    this.bloomTargets = [];

    if (this.captureTarget) {
      this.captureTarget.destroy();
      this.captureTarget = null;
    }

    if (this.lightingTarget) {
      this.lightingTarget.destroy();
      this.lightingTarget = null;
    }

    if (this.temporalTarget) {
      this.temporalTarget.destroy();
      this.temporalTarget = null;
    }

    for (const target of this.temporalHistoryTargets) {
      target.destroy();
    }
    this.temporalHistoryTargets = [];

    if (this.denoiseTarget) {
      this.denoiseTarget.destroy();
      this.denoiseTarget = null;
    }

    this.temporalPhase = 0;
    this.temporalCount = 0;
    this.hdrReady = false;
  }

  private resolveHdrMode(): HdrTextureConfig | null {
    const gl = this.gl as GL;

    if (typeof WebGL2RenderingContext !== "undefined" && gl instanceof WebGL2RenderingContext) {
      const colorBufferFloat = gl.getExtension("EXT_color_buffer_float");
      if (!colorBufferFloat) {
        return null;
      }

      return {
        internalFormat: gl.RGBA16F,
        format: gl.RGBA,
        type: gl.HALF_FLOAT,
        label: "RGBA16F"
      };
    }

    const gl1 = gl as WebGLRenderingContext;

    const halfFloat = gl1.getExtension("OES_texture_half_float") as { HALF_FLOAT_OES: number } | null;
    const halfFloatLinear = gl1.getExtension("OES_texture_half_float_linear");
    const halfColorBuffer = gl1.getExtension("EXT_color_buffer_half_float");

    if (halfFloat && halfFloatLinear && halfColorBuffer) {
      return {
        internalFormat: gl1.RGBA,
        format: gl1.RGBA,
        type: halfFloat.HALF_FLOAT_OES,
        label: "RGBA16F(WebGL1)"
      };
    }

    const floatTex = gl1.getExtension("OES_texture_float");
    const floatLinear = gl1.getExtension("OES_texture_float_linear");
    const floatColorBuffer = gl1.getExtension("WEBGL_color_buffer_float");

    if (floatTex && floatLinear && floatColorBuffer) {
      return {
        internalFormat: gl1.RGBA,
        format: gl1.RGBA,
        type: gl1.FLOAT,
        label: "RGBA32F(WebGL1)"
      };
    }

    return null;
  }

  private runPrefilter(source: HdrRenderTarget, target: HdrRenderTarget): void {
    const shader = this.shaders[4] as WebGLShader;

    this.bind(shader);
    this.set1f("uBloomInputGain", this.bloomInputGain);
    this.set1f("uBloomCompression", this.bloomCompression);
    this.set1f("uSceneBloomFloor", this.sceneBloomFloor);
    this.set1f("uSceneBloomBias", this.sceneBloomBias);

    this.bindAndDraw(source as unknown as RenderTargetLike, target as unknown as RenderTargetLike, true, true, shader);
  }

  private clearRenderTarget(target: HdrRenderTarget): void {
    const renderer = this.renderer as unknown as WebGLRenderer;
    target.bind(true);
    renderer.popFramebuffer();
  }

  private runDownsample(source: HdrRenderTarget, target: HdrRenderTarget): void {
    const shader = this.shaders[5] as WebGLShader;

    this.bind(shader);
    this.set2f("uTexelSize", 1 / source.width, 1 / source.height);
    this.set1f("uRadius", this.bloomRadius);

    this.bindAndDraw(source as unknown as RenderTargetLike, target as unknown as RenderTargetLike, true, true, shader);
  }

  private runUpsample(high: HdrRenderTarget, low: HdrRenderTarget, target: HdrRenderTarget): void {
    const gl = this.gl;
    const shader = this.shaders[6] as WebGLShader;

    this.bind(shader);
    this.set1i("uLowSampler", 1);
    this.set2f("uLowTexelSize", 1 / low.width, 1 / low.height);
    this.set1f("uRadius", this.bloomRadius);
    this.set1f("uLowWeight", this.upsampleWeight);

    gl.activeTexture(gl.TEXTURE1);
    gl.bindTexture(gl.TEXTURE_2D, low.texture.webGLTexture);

    this.bindAndDraw(high as unknown as RenderTargetLike, target as unknown as RenderTargetLike, true, true, shader);

    gl.activeTexture(gl.TEXTURE1);
    gl.bindTexture(gl.TEXTURE_2D, null);
    gl.activeTexture(gl.TEXTURE0);
  }

  private runComposite(sceneSource: HdrRenderTarget, bloomSource: HdrRenderTarget, lightSource: HdrRenderTarget): void {
    const gl = this.gl;
    const shader = this.shaders[7] as WebGLShader;

    this.bind(shader);
    this.set1i("uBloomSampler", 1);
    this.set1i("uLightSampler", 2);
    this.set1f("uBloomStrength", this.bloomStrength);
    this.set1f("uExposure", this.hdrExposure);

    gl.activeTexture(gl.TEXTURE1);
    gl.bindTexture(gl.TEXTURE_2D, bloomSource.texture.webGLTexture);
    gl.activeTexture(gl.TEXTURE2);
    gl.bindTexture(gl.TEXTURE_2D, lightSource.texture.webGLTexture);

    this.bindAndDraw(sceneSource as unknown as RenderTargetLike, undefined, true, true, shader);

    gl.activeTexture(gl.TEXTURE2);
    gl.bindTexture(gl.TEXTURE_2D, null);
    gl.activeTexture(gl.TEXTURE1);
    gl.bindTexture(gl.TEXTURE_2D, null);
    gl.activeTexture(gl.TEXTURE0);
  }

  private runLighting(source: HdrRenderTarget, target: HdrRenderTarget): void {
    const shader = this.shaders[0] as WebGLShader;

    this.bind(shader);
    this.set2f("uSceneSize", source.width, source.height);
    this.set1f("uFrameSeed", this.frameSeed);
    this.set2f("uCameraOffset", this.cameraOffsetX, this.cameraOffsetY);
    this.set4f("uSceneRect", this.sceneRect[0], this.sceneRect[1], this.sceneRect[2], this.sceneRect[3]);
    this.set1f("uLightVisibility", this.lightVisibility);

    this.set1i("uSegmentCount", this.segmentCount);
    this.set4fv("uLightSegments", this.lightSegments);
    this.set3fv("uLightColors", this.lightColors);

    this.set1i("uBoxCount", this.boxCount);
    this.set4fv("uShadowBoxes", this.shadowBoxes);

    this.set1i("uCircleCount", this.circleCount);
    this.set3fv("uShadowCircles", this.shadowCircles);

    this.set1i("uSampleCount", this.sampleCount);
    this.set1f("uLightIntensity", this.lightIntensity);
    this.set1f("uMinDistanceSq", this.minDistanceSq);
    this.set1f("uMaxContribution", this.maxContribution);

    this.bindAndDraw(source as unknown as RenderTargetLike, target as unknown as RenderTargetLike, true, true, shader);
  }

  private runDenoise(source: HdrRenderTarget, guide: HdrRenderTarget, target: HdrRenderTarget): void {
    const gl = this.gl;
    const shader = this.shaders[3] as WebGLShader;

    this.bind(shader);
    this.set1i("uGuideSampler", 1);
    this.set2f("uTexelSize", 1 / source.width, 1 / source.height);
    this.set1f("uDenoiseSpatialInvSigma2", this.denoiseSpatialInvSigma2);
    this.set1f("uDenoiseColorInvSigma2", this.denoiseColorInvSigma2);
    this.set1f("uDenoiseRadiusSq", this.denoiseRadiusSq);
    this.set1f("uDenoiseBlend", this.denoiseBlend);

    gl.activeTexture(gl.TEXTURE1);
    gl.bindTexture(gl.TEXTURE_2D, guide.texture.webGLTexture);

    this.bindAndDraw(source as unknown as RenderTargetLike, target as unknown as RenderTargetLike, true, true, shader);

    gl.activeTexture(gl.TEXTURE1);
    gl.bindTexture(gl.TEXTURE_2D, null);
    gl.activeTexture(gl.TEXTURE0);
  }

  private runCopy(source: HdrRenderTarget, target: HdrRenderTarget): void {
    const shader = this.shaders[1] as WebGLShader;
    this.bind(shader);
    this.bindAndDraw(source as unknown as RenderTargetLike, target as unknown as RenderTargetLike, true, true, shader);
  }

  private runTemporalAccum(target: HdrRenderTarget, temporalCount: number, newestIndex: number): void {
    const gl = this.gl;
    const shader = this.shaders[2] as WebGLShader;
    const count = Phaser.Math.Clamp(temporalCount, 0, TEMPORAL_HISTORY_FRAMES);
    const len = this.temporalHistoryTargets.length;
    const wrap = (index: number): number => ((index % len) + len) % len;
    const newest = wrap(newestIndex);
    const history1 = this.temporalHistoryTargets[wrap(newest - 1)];
    const history2 = this.temporalHistoryTargets[wrap(newest - 2)];
    const history3 = this.temporalHistoryTargets[wrap(newest - 3)];
    const history4 = this.temporalHistoryTargets[wrap(newest - 4)];

    this.bind(shader);
    this.set1i("uHistory1", 1);
    this.set1i("uHistory2", 2);
    this.set1i("uHistory3", 3);
    this.set1i("uHistory4", 4);
    this.set1i("uTemporalCount", count);
    this.set1f("uTemporalDisocclusionThreshold", this.temporalDisocclusionThreshold);

    gl.activeTexture(gl.TEXTURE1);
    gl.bindTexture(gl.TEXTURE_2D, history1.texture.webGLTexture);
    gl.activeTexture(gl.TEXTURE2);
    gl.bindTexture(gl.TEXTURE_2D, history2.texture.webGLTexture);
    gl.activeTexture(gl.TEXTURE3);
    gl.bindTexture(gl.TEXTURE_2D, history3.texture.webGLTexture);
    gl.activeTexture(gl.TEXTURE4);
    gl.bindTexture(gl.TEXTURE_2D, history4.texture.webGLTexture);

    this.bindAndDraw(
      this.temporalHistoryTargets[newest] as unknown as RenderTargetLike,
      target as unknown as RenderTargetLike,
      true,
      true,
      shader
    );

    gl.activeTexture(gl.TEXTURE4);
    gl.bindTexture(gl.TEXTURE_2D, null);
    gl.activeTexture(gl.TEXTURE3);
    gl.bindTexture(gl.TEXTURE_2D, null);
    gl.activeTexture(gl.TEXTURE2);
    gl.bindTexture(gl.TEXTURE_2D, null);
    gl.activeTexture(gl.TEXTURE1);
    gl.bindTexture(gl.TEXTURE_2D, null);
    gl.activeTexture(gl.TEXTURE0);
  }
}
