<template>
  <div ref="mountRef" :class="`w-full h-full relative overflow-hidden ${className || ''}`" :style="style" />
</template>

<script setup lang="ts">
import * as THREE from 'three';
import { onMounted, onUnmounted, ref, watch } from 'vue';

interface LiquidEtherProps {
  mouseForce?: number;
  cursorSize?: number;
  isViscous?: boolean;
  viscous?: number;
  iterationsViscous?: number;
  iterationsPoisson?: number;
  dt?: number;
  BFECC?: boolean;
  resolution?: number;
  isBounce?: boolean;
  colors?: string[];
  style?: Record<string, any>;
  className?: string;
  autoDemo?: boolean;
  autoSpeed?: number;
  autoIntensity?: number;
  takeoverDuration?: number;
  autoResumeDelay?: number;
  autoRampDuration?: number;
}

interface SimOptions {
  iterations_poisson: number;
  iterations_viscous: number;
  mouse_force: number;
  resolution: number;
  cursor_size: number;
  viscous: number;
  isBounce: boolean;
  dt: number;
  isViscous: boolean;
  BFECC: boolean;
}

interface LiquidEtherWebGL {
  output?: { simulation?: { options: SimOptions; resize: () => void } };
  autoDriver?: {
    enabled: boolean;
    speed: number;
    resumeDelay: number;
    rampDurationMs: number;
    mouse?: { autoIntensity: number; takeoverDuration: number };
    forceStop: () => void;
  };
  resize: () => void;
  start: () => void;
  pause: () => void;
  dispose: () => void;
}

const props = withDefaults(defineProps<LiquidEtherProps>(), {
  mouseForce: 20,
  cursorSize: 100,
  isViscous: false,
  viscous: 30,
  iterationsViscous: 32,
  iterationsPoisson: 32,
  dt: 0.014,
  BFECC: true,
  resolution: 0.5,
  isBounce: false,
  colors: () => ['#5227FF', '#FF9FFC', '#B19EEF'],
  style: () => ({}),
  className: '',
  autoDemo: true,
  autoSpeed: 0.5,
  autoIntensity: 2.2,
  takeoverDuration: 0.25,
  autoResumeDelay: 1000,
  autoRampDuration: 0.6
});

const mountRef = ref<HTMLDivElement | null>(null);
const webglRef = ref<LiquidEtherWebGL | null>(null);
const resizeObserverRef = ref<ResizeObserver | null>(null);
const rafRef = ref<number | null>(null);
const intersectionObserverRef = ref<IntersectionObserver | null>(null);
const isVisibleRef = ref<boolean>(true);
const resizeRafRef = ref<number | null>(null);

const initWebGL = () => {
  if (!mountRef.value) return;

  function makePaletteTexture(stops: string[]): THREE.DataTexture {
    let arr: string[];
    if (Array.isArray(stops) && stops.length > 0) {
      arr = stops.length === 1 ? [stops[0], stops[0]] : stops;
    } else {
      arr = ['#ffffff', '#ffffff'];
    }
    const w = arr.length;
    const data = new Uint8Array(w * 4);
    for (let i = 0; i < w; i++) {
      const c = new THREE.Color(arr[i]);
      data[i * 4 + 0] = Math.round(c.r * 255);
      data[i * 4 + 1] = Math.round(c.g * 255);
      data[i * 4 + 2] = Math.round(c.b * 255);
      data[i * 4 + 3] = 255;
    }
    const tex = new THREE.DataTexture(data, w, 1, THREE.RGBAFormat);
    tex.magFilter = THREE.LinearFilter;
    tex.minFilter = THREE.LinearFilter;
    tex.wrapS = THREE.ClampToEdgeWrapping;
    tex.wrapT = THREE.ClampToEdgeWrapping;
    tex.generateMipmaps = false;
    tex.needsUpdate = true;
    return tex;
  }

  const paletteTex = makePaletteTexture(props.colors);
  const bgVec4 = new THREE.Vector4(0, 0, 0, 0);

  class CommonClass {
    width = 0;
    height = 0;
    aspect = 1;
    pixelRatio = 1;
    isMobile = false;
    breakpoint = 768;
    fboWidth: number | null = null;
    fboHeight: number | null = null;
    time = 0;
    delta = 0;
    container: HTMLElement | null = null;
    renderer: THREE.WebGLRenderer | null = null;
    clock: THREE.Clock | null = null;

    init(container: HTMLElement) {
      this.container = container;
      this.pixelRatio = Math.min(window.devicePixelRatio || 1, 2);
      this.resize();
      this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
      this.renderer.autoClear = false;
      this.renderer.setClearColor(new THREE.Color(0x000000), 0);
      this.renderer.setPixelRatio(this.pixelRatio);
      this.renderer.setSize(this.width, this.height);
      const el = this.renderer.domElement;
      el.style.width = '100%';
      el.style.height = '100%';
      el.style.display = 'block';
      this.clock = new THREE.Clock();
      this.clock.start();
    }

    resize() {
      if (!this.container) return;
      const rect = this.container.getBoundingClientRect();
      this.width = Math.max(1, Math.floor(rect.width));
      this.height = Math.max(1, Math.floor(rect.height));
      this.aspect = this.width / this.height;
      if (this.renderer) this.renderer.setSize(this.width, this.height, false);
    }

    update() {
      if (!this.clock) return;
      this.delta = this.clock.getDelta();
      this.time += this.delta;
    }
  }
  const Common = new CommonClass();

  class MouseClass {
    mouseMoved = false;
    coords = new THREE.Vector2();
    coords_old = new THREE.Vector2();
    diff = new THREE.Vector2();
    timer: number | null = null;
    container: HTMLElement | null = null;
    isHoverInside = false;
    hasUserControl = false;
    isAutoActive = false;
    autoIntensity = 2.0;
    takeoverActive = false;
    takeoverStartTime = 0;
    takeoverDuration = 0.25;
    takeoverFrom = new THREE.Vector2();
    takeoverTo = new THREE.Vector2();
    onInteract: (() => void) | null = null;
    private _onMouseMove = this.onDocumentMouseMove.bind(this);
    private _onTouchStart = this.onDocumentTouchStart.bind(this);
    private _onTouchMove = this.onDocumentTouchMove.bind(this);
    private _onMouseEnter = this.onMouseEnter.bind(this);
    private _onMouseLeave = this.onMouseLeave.bind(this);
    private _onTouchEnd = this.onTouchEnd.bind(this);

    init(container: HTMLElement) {
      this.container = container;
      container.addEventListener('mousemove', this._onMouseMove);
      container.addEventListener('touchstart', this._onTouchStart, { passive: true });
      container.addEventListener('touchmove', this._onTouchMove, { passive: true });
      container.addEventListener('mouseenter', this._onMouseEnter);
      container.addEventListener('mouseleave', this._onMouseLeave);
      container.addEventListener('touchend', this._onTouchEnd);
    }

    dispose() {
      const c = this.container;
      if (!c) return;
      c.removeEventListener('mousemove', this._onMouseMove);
      c.removeEventListener('touchstart', this._onTouchStart);
      c.removeEventListener('touchmove', this._onTouchMove);
      c.removeEventListener('mouseenter', this._onMouseEnter);
      c.removeEventListener('mouseleave', this._onMouseLeave);
      c.removeEventListener('touchend', this._onTouchEnd);
    }

    setCoords(x: number, y: number) {
      if (!this.container) return;
      if (this.timer) window.clearTimeout(this.timer);
      const rect = this.container.getBoundingClientRect();
      const nx = (x - rect.left) / rect.width;
      const ny = (y - rect.top) / rect.height;
      this.coords.set(nx * 2 - 1, -(ny * 2 - 1));
      this.mouseMoved = true;
      this.timer = window.setTimeout(() => {
        this.mouseMoved = false;
      }, 100);
    }

    setNormalized(nx: number, ny: number) {
      this.coords.set(nx, ny);
      this.mouseMoved = true;
    }

    onDocumentMouseMove(event: MouseEvent) {
      if (this.onInteract) this.onInteract();
      if (this.isAutoActive && !this.hasUserControl && !this.takeoverActive) {
        if (!this.container) return;
        const rect = this.container.getBoundingClientRect();
        const nx = (event.clientX - rect.left) / rect.width;
        const ny = (event.clientY - rect.top) / rect.height;
        this.takeoverFrom.copy(this.coords);
        this.takeoverTo.set(nx * 2 - 1, -(ny * 2 - 1));
        this.takeoverStartTime = performance.now();
        this.takeoverActive = true;
        this.hasUserControl = true;
        this.isAutoActive = false;
        return;
      }
      this.setCoords(event.clientX, event.clientY);
      this.hasUserControl = true;
    }

    onDocumentTouchStart(event: TouchEvent) {
      if (event.touches.length === 1) {
        const t = event.touches[0];
        if (this.onInteract) this.onInteract();
        this.setCoords(t.pageX, t.pageY);
        this.hasUserControl = true;
      }
    }

    onDocumentTouchMove(event: TouchEvent) {
      if (event.touches.length === 1) {
        const t = event.touches[0];
        if (this.onInteract) this.onInteract();
        this.setCoords(t.pageX, t.pageY);
      }
    }

    onTouchEnd() {
      this.isHoverInside = false;
    }

    onMouseEnter() {
      this.isHoverInside = true;
    }

    onMouseLeave() {
      this.isHoverInside = false;
    }

    update() {
      if (this.takeoverActive) {
        const t = (performance.now() - this.takeoverStartTime) / (this.takeoverDuration * 1000);
        if (t >= 1) {
          this.takeoverActive = false;
          this.coords.copy(this.takeoverTo);
          this.coords_old.copy(this.coords);
          this.diff.set(0, 0);
        } else {
          const k = t * t * (3 - 2 * t);
          this.coords.copy(this.takeoverFrom).lerp(this.takeoverTo, k);
        }
      }
      this.diff.subVectors(this.coords, this.coords_old);
      this.coords_old.copy(this.coords);
      if (this.coords_old.x === 0 && this.coords_old.y === 0) this.diff.set(0, 0);
      if (this.isAutoActive && !this.takeoverActive) this.diff.multiplyScalar(this.autoIntensity);
    }
  }
  const Mouse = new MouseClass();

  class AutoDriver {
    mouse: MouseClass;
    manager: WebGLManager;
    enabled: boolean;
    speed: number;
    resumeDelay: number;
    rampDurationMs: number;
    active = false;
    current = new THREE.Vector2(0, 0);
    target = new THREE.Vector2();
    lastTime = performance.now();
    activationTime = 0;
    margin = 0.2;
    private _tmpDir = new THREE.Vector2();

    constructor(
      mouse: MouseClass,
      manager: WebGLManager,
      opts: { enabled: boolean; speed: number; resumeDelay: number; rampDuration: number }
    ) {
      this.mouse = mouse;
      this.manager = manager;
      this.enabled = opts.enabled;
      this.speed = opts.speed;
      this.resumeDelay = opts.resumeDelay || 3000;
      this.rampDurationMs = (opts.rampDuration || 0) * 1000;
      this.pickNewTarget();
    }

    pickNewTarget() {
      const r = Math.random;
      this.target.set((r() * 2 - 1) * (1 - this.margin), (r() * 2 - 1) * (1 - this.margin));
    }

    forceStop() {
      this.active = false;
      this.mouse.isAutoActive = false;
    }

    update() {
      if (!this.enabled) return;
      const now = performance.now();
      const idle = now - this.manager.lastUserInteraction;
      if (idle < this.resumeDelay) {
        if (this.active) this.forceStop();
        return;
      }
      if (this.mouse.isHoverInside) {
        if (this.active) this.forceStop();
        return;
      }
      if (!this.active) {
        this.active = true;
        this.current.copy(this.mouse.coords);
        this.lastTime = now;
        this.activationTime = now;
      }
      if (!this.active) return;
      this.mouse.isAutoActive = true;
      let dtSec = (now - this.lastTime) / 1000;
      this.lastTime = now;
      if (dtSec > 0.2) dtSec = 0.016;
      const dir = this._tmpDir.subVectors(this.target, this.current);
      const dist = dir.length();
      if (dist < 0.01) {
        this.pickNewTarget();
        return;
      }
      dir.normalize();
      let ramp = 1;
      if (this.rampDurationMs > 0) {
        const t = Math.min(1, (now - this.activationTime) / this.rampDurationMs);
        ramp = t * t * (3 - 2 * t);
      }
      const step = this.speed * dtSec * ramp;
      const move = Math.min(step, dist);
      this.current.addScaledVector(dir, move);
      this.mouse.setNormalized(this.current.x, this.current.y);
    }
  }

  // Shader code
  const face_vert = `
    attribute vec3 position;
    uniform vec2 px;
    uniform vec2 boundarySpace;
    varying vec2 uv;
    precision highp float;
    void main(){
    vec3 pos = position;
    vec2 scale = 1.0 - boundarySpace * 2.0;
    pos.xy = pos.xy * scale;
    uv = vec2(0.5)+(pos.xy)*0.5;
    gl_Position = vec4(pos, 1.0);
  }
  `;
  const line_vert = `
    attribute vec3 position;
    uniform vec2 px;
    precision highp float;
    varying vec2 uv;
    void main(){
    vec3 pos = position;
    uv = 0.5 + pos.xy * 0.5;
    vec2 n = sign(pos.xy);
    pos.xy = abs(pos.xy) - px * 1.0;
    pos.xy *= n;
    gl_Position = vec4(pos, 1.0);
  }
  `;
  const mouse_vert = `
      precision highp float;
      attribute vec3 position;
      attribute vec2 uv;
      uniform vec2 center;
      uniform vec2 scale;
      uniform vec2 px;
      varying vec2 vUv;
      void main(){
      vec2 pos = position.xy * scale * 2.0 * px + center;
      vUv = uv;
      gl_Position = vec4(pos, 0.0, 1.0);
  }
  `;
  const advection_frag = `
      precision highp float;
      uniform sampler2D velocity;
      uniform float dt;
      uniform bool isBFECC;
      uniform vec2 fboSize;
      uniform vec2 px;
      varying vec2 uv;
      void main(){
      vec2 ratio = max(fboSize.x, fboSize.y) / fboSize;
      if(isBFECC == false){
          vec2 vel = texture2D(velocity, uv).xy;
          vec2 uv2 = uv - vel * dt * ratio;
          vec2 newVel = texture2D(velocity, uv2).xy;
          gl_FragColor = vec4(newVel, 0.0, 0.0);
      } else {
          vec2 spot_new = uv;
          vec2 vel_old = texture2D(velocity, uv).xy;
          vec2 spot_old = spot_new - vel_old * dt * ratio;
          vec2 vel_new1 = texture2D(velocity, spot_old).xy;
          vec2 spot_new2 = spot_old + vel_new1 * dt * ratio;
          vec2 error = spot_new2 - spot_new;
          vec2 spot_new3 = spot_new - error / 2.0;
          vec2 vel_2 = texture2D(velocity, spot_new3).xy;
          vec2 spot_old2 = spot_new3 - vel_2 * dt * ratio;
          vec2 newVel2 = texture2D(velocity, spot_old2).xy; 
          gl_FragColor = vec4(newVel2, 0.0, 0.0);
      }
  }
  `;
  const color_frag = `
      precision highp float;
      uniform sampler2D velocity;
      uniform sampler2D palette;
      uniform vec4 bgColor;
      varying vec2 uv;
      void main(){
      vec2 vel = texture2D(velocity, uv).xy;
      float lenv = clamp(length(vel), 0.0, 1.0);
      vec3 c = texture2D(palette, vec2(lenv, 0.5)).rgb;
      vec3 outRGB = mix(bgColor.rgb, c, lenv);
      float outA = mix(bgColor.a, 1.0, lenv);
      gl_FragColor = vec4(outRGB, outA);
  }
  `;
  const divergence_frag = `
      precision highp float;
      uniform sampler2D velocity;
      uniform float dt;
      uniform vec2 px;
      varying vec2 uv;
      void main(){
      float x0 = texture2D(velocity, uv-vec2(px.x, 0.0)).x;
      float x1 = texture2D(velocity, uv+vec2(px.x, 0.0)).x;
      float y0 = texture2D(velocity, uv-vec2(0.0, px.y)).y;
      float y1 = texture2D(velocity, uv+vec2(0.0, px.y)).y;
      float divergence = (x1 - x0 + y1 - y0) / 2.0;
      gl_FragColor = vec4(divergence / dt);
  }
  `;
  const externalForce_frag = `
      precision highp float;
      uniform vec2 force;
      uniform vec2 center;
      uniform vec2 scale;
      uniform vec2 px;
      varying vec2 vUv;
      void main(){
      vec2 circle = (vUv - 0.5) * 2.0;
      float d = 1.0 - min(length(circle), 1.0);
      d *= d;
      gl_FragColor = vec4(force * d, 0.0, 1.0);
  }
  `;
  const poisson_frag = `
      precision highp float;
      uniform sampler2D pressure;
      uniform sampler2D divergence;
      uniform vec2 px;
      varying vec2 uv;
      void main(){
      float p0 = texture2D(pressure, uv + vec2(px.x * 2.0, 0.0)).r;
      float p1 = texture2D(pressure, uv - vec2(px.x * 2.0, 0.0)).r;
      float p2 = texture2D(pressure, uv + vec2(0.0, px.y * 2.0)).r;
      float p3 = texture2D(pressure, uv - vec2(0.0, px.y * 2.0)).r;
      float div = texture2D(divergence, uv).r;
      float newP = (p0 + p1 + p2 + p3) / 4.0 - div;
      gl_FragColor = vec4(newP);
  }
  `;
  const pressure_frag = `
      precision highp float;
      uniform sampler2D pressure;
      uniform sampler2D velocity;
      uniform vec2 px;
      uniform float dt;
      varying vec2 uv;
      void main(){
      float step = 1.0;
      float p0 = texture2D(pressure, uv + vec2(px.x * step, 0.0)).r;
      float p1 = texture2D(pressure, uv - vec2(px.x * step, 0.0)).r;
      float p2 = texture2D(pressure, uv + vec2(0.0, px.y * step)).r;
      float p3 = texture2D(pressure, uv - vec2(0.0, px.y * step)).r;
      vec2 v = texture2D(velocity, uv).xy;
      vec2 gradP = vec2(p0 - p1, p2 - p3) * 0.5;
      v = v - gradP * dt;
      gl_FragColor = vec4(v, 0.0, 1.0);
  }
  `;
  const viscous_frag = `
      precision highp float;
      uniform sampler2D velocity;
      uniform sampler2D velocity_new;
      uniform float v;
      uniform vec2 px;
      uniform float dt;
      varying vec2 uv;
      void main(){
      vec2 old = texture2D(velocity, uv).xy;
      vec2 new0 = texture2D(velocity_new, uv + vec2(px.x * 2.0, 0.0)).xy;
      vec2 new1 = texture2D(velocity_new, uv - vec2(px.x * 2.0, 0.0)).xy;
      vec2 new2 = texture2D(velocity_new, uv + vec2(0.0, px.y * 2.0)).xy;
      vec2 new3 = texture2D(velocity_new, uv - vec2(0.0, px.y * 2.0)).xy;
      vec2 newv = 4.0 * old + v * dt * (new0 + new1 + new2 + new3);
      newv /= 4.0 * (1.0 + v * dt);
      gl_FragColor = vec4(newv, 0.0, 0.0);
  }
  `;

  type Uniforms = Record<string, { value: any }>;

  class ShaderPass {
    props: any;
    uniforms?: Uniforms;
    scene: THREE.Scene | null = null;
    camera: THREE.Camera | null = null;
    material: THREE.RawShaderMaterial | null = null;
    geometry: THREE.BufferGeometry | null = null;
    plane: THREE.Mesh | null = null;

    constructor(props: any) {
      this.props = props || {};
      this.uniforms = this.props.material?.uniforms;
    }

    init(..._args: any[]) {
      this.scene = new THREE.Scene();
      this.camera = new THREE.Camera();
      if (this.uniforms) {
        this.material = new THREE.RawShaderMaterial(this.props.material);
        this.geometry = new THREE.PlaneGeometry(2, 2);
        this.plane = new THREE.Mesh(this.geometry, this.material);
        this.scene.add(this.plane);
      }
    }

    update(..._args: any[]) {
      if (!Common.renderer || !this.scene || !this.camera) return;
      Common.renderer.setRenderTarget(this.props.output || null);
      Common.renderer.render(this.scene, this.camera);
      Common.renderer.setRenderTarget(null);
    }
  }

  // Shader pass classes (Advection, ExternalForce, etc.) - keeping them the same as in React version
  class Advection extends ShaderPass {
    line!: THREE.LineSegments;

    constructor(simProps: any) {
      super({
        material: {
          vertexShader: face_vert,
          fragmentShader: advection_frag,
          uniforms: {
            boundarySpace: { value: simProps.cellScale },
            px: { value: simProps.cellScale },
            fboSize: { value: simProps.fboSize },
            velocity: { value: simProps.src.texture },
            dt: { value: simProps.dt },
            isBFECC: { value: true }
          }
        },
        output: simProps.dst
      });
      this.uniforms = this.props.material.uniforms;
      this.init();
    }

    init() {
      super.init();
      this.createBoundary();
    }

    createBoundary() {
      const boundaryG = new THREE.BufferGeometry();
      const vertices_boundary = new Float32Array([
        -1, -1, 0, -1, 1, 0, -1, 1, 0, 1, 1, 0, 1, 1, 0, 1, -1, 0, 1, -1, 0, -1, -1, 0
      ]);
      boundaryG.setAttribute('position', new THREE.BufferAttribute(vertices_boundary, 3));
      const boundaryM = new THREE.RawShaderMaterial({
        vertexShader: line_vert,
        fragmentShader: advection_frag,
        uniforms: this.uniforms!
      });
      this.line = new THREE.LineSegments(boundaryG, boundaryM);
      this.scene!.add(this.line);
    }

    update(...args: any[]) {
      const { dt, isBounce, BFECC } = (args[0] || {}) as { dt?: number; isBounce?: boolean; BFECC?: boolean };
      if (!this.uniforms) return;
      if (typeof dt === 'number') this.uniforms.dt.value = dt;
      if (typeof isBounce === 'boolean') this.line.visible = isBounce;
      if (typeof BFECC === 'boolean') this.uniforms.isBFECC.value = BFECC;
      super.update();
    }
  }

  class ExternalForce extends ShaderPass {
    mouse!: THREE.Mesh;

    constructor(simProps: any) {
      super({ output: simProps.dst });
      this.init(simProps);
    }

    init(simProps: any) {
      super.init();
      const mouseG = new THREE.PlaneGeometry(1, 1);
      const mouseM = new THREE.RawShaderMaterial({
        vertexShader: mouse_vert,
        fragmentShader: externalForce_frag,
        blending: THREE.AdditiveBlending,
        depthWrite: false,
        uniforms: {
          px: { value: simProps.cellScale },
          force: { value: new THREE.Vector2(0, 0) },
          center: { value: new THREE.Vector2(0, 0) },
          scale: { value: new THREE.Vector2(simProps.cursor_size, simProps.cursor_size) }
        }
      });
      this.mouse = new THREE.Mesh(mouseG, mouseM);
      this.scene!.add(this.mouse);
    }

    update(...args: any[]) {
      const props = args[0] || {};
      const forceX = (Mouse.diff.x / 2) * (props.mouse_force || 0);
      const forceY = (Mouse.diff.y / 2) * (props.mouse_force || 0);
      const cellScale = props.cellScale || { x: 1, y: 1 };
      const cursorSize = props.cursor_size || 0;
      const cursorSizeX = cursorSize * cellScale.x;
      const cursorSizeY = cursorSize * cellScale.y;
      const centerX = Math.min(
        Math.max(Mouse.coords.x, -1 + cursorSizeX + cellScale.x * 2),
        1 - cursorSizeX - cellScale.x * 2
      );
      const centerY = Math.min(
        Math.max(Mouse.coords.y, -1 + cursorSizeY + cellScale.y * 2),
        1 - cursorSizeY - cellScale.y * 2
      );
      const uniforms = (this.mouse.material as THREE.RawShaderMaterial).uniforms;
      uniforms.force.value.set(forceX, forceY);
      uniforms.center.value.set(centerX, centerY);
      uniforms.scale.value.set(cursorSize, cursorSize);
      super.update();
    }
  }

  class Viscous extends ShaderPass {
    constructor(simProps: any) {
      super({
        material: {
          vertexShader: face_vert,
          fragmentShader: viscous_frag,
          uniforms: {
            boundarySpace: { value: simProps.boundarySpace },
            velocity: { value: simProps.src.texture },
            velocity_new: { value: simProps.dst_.texture },
            v: { value: simProps.viscous },
            px: { value: simProps.cellScale },
            dt: { value: simProps.dt }
          }
        },
        output: simProps.dst,
        output0: simProps.dst_,
        output1: simProps.dst
      });
      this.init();
    }

    update(...args: any[]) {
      const { viscous, iterations, dt } = (args[0] || {}) as { viscous?: number; iterations?: number; dt?: number };
      if (!this.uniforms) return;
      let fbo_in: any, fbo_out: any;
      if (typeof viscous === 'number') this.uniforms.v.value = viscous;
      const iter = iterations ?? 0;
      for (let i = 0; i < iter; i++) {
        if (i % 2 === 0) {
          fbo_in = this.props.output0;
          fbo_out = this.props.output1;
        } else {
          fbo_in = this.props.output1;
          fbo_out = this.props.output0;
        }
        this.uniforms.velocity_new.value = fbo_in.texture;
        this.props.output = fbo_out;
        if (typeof dt === 'number') this.uniforms.dt.value = dt;
        super.update();
      }
      return fbo_out;
    }
  }

  class Divergence extends ShaderPass {
    constructor(simProps: any) {
      super({
        material: {
          vertexShader: face_vert,
          fragmentShader: divergence_frag,
          uniforms: {
            boundarySpace: { value: simProps.boundarySpace },
            velocity: { value: simProps.src.texture },
            px: { value: simProps.cellScale },
            dt: { value: simProps.dt }
          }
        },
        output: simProps.dst
      });
      this.init();
    }

    update(...args: any[]) {
      const { vel } = (args[0] || {}) as { vel?: any };
      if (this.uniforms && vel) {
        this.uniforms.velocity.value = vel.texture;
      }
      super.update();
    }
  }

  class Poisson extends ShaderPass {
    constructor(simProps: any) {
      super({
        material: {
          vertexShader: face_vert,
          fragmentShader: poisson_frag,
          uniforms: {
            boundarySpace: { value: simProps.boundarySpace },
            pressure: { value: simProps.dst_.texture },
            divergence: { value: simProps.src.texture },
            px: { value: simProps.cellScale }
          }
        },
        output: simProps.dst,
        output0: simProps.dst_,
        output1: simProps.dst
      });
      this.init();
    }

    update(...args: any[]) {
      const { iterations } = (args[0] || {}) as { iterations?: number };
      let p_in: any, p_out: any;
      const iter = iterations ?? 0;
      for (let i = 0; i < iter; i++) {
        if (i % 2 === 0) {
          p_in = this.props.output0;
          p_out = this.props.output1;
        } else {
          p_in = this.props.output1;
          p_out = this.props.output0;
        }
        if (this.uniforms) this.uniforms.pressure.value = p_in.texture;
        this.props.output = p_out;
        super.update();
      }
      return p_out;
    }
  }

  class Pressure extends ShaderPass {
    constructor(simProps: any) {
      super({
        material: {
          vertexShader: face_vert,
          fragmentShader: pressure_frag,
          uniforms: {
            boundarySpace: { value: simProps.boundarySpace },
            pressure: { value: simProps.src_p.texture },
            velocity: { value: simProps.src_v.texture },
            px: { value: simProps.cellScale },
            dt: { value: simProps.dt }
          }
        },
        output: simProps.dst
      });
      this.init();
    }

    update(...args: any[]) {
      const { vel, pressure } = (args[0] || {}) as { vel?: any; pressure?: any };
      if (this.uniforms && vel && pressure) {
        this.uniforms.velocity.value = vel.texture;
        this.uniforms.pressure.value = pressure.texture;
      }
      super.update();
    }
  }

  class Simulation {
    options: SimOptions;
    fbos: Record<string, THREE.WebGLRenderTarget | null> = {
      vel_0: null,
      vel_1: null,
      vel_viscous0: null,
      vel_viscous1: null,
      div: null,
      pressure_0: null,
      pressure_1: null
    };
    fboSize = new THREE.Vector2();
    cellScale = new THREE.Vector2();
    boundarySpace = new THREE.Vector2();
    advection!: Advection;
    externalForce!: ExternalForce;
    viscous!: Viscous;
    divergence!: Divergence;
    poisson!: Poisson;
    pressure!: Pressure;

    constructor(options?: Partial<SimOptions>) {
      this.options = {
        iterations_poisson: 32,
        iterations_viscous: 32,
        mouse_force: 20,
        resolution: 0.5,
        cursor_size: 100,
        viscous: 30,
        isBounce: false,
        dt: 0.014,
        isViscous: false,
        BFECC: true,
        ...options
      };
      this.init();
    }

    init() {
      this.calcSize();
      this.createAllFBO();
      this.createShaderPass();
    }

    getFloatType() {
      const isIOS = /(iPad|iPhone|iPod)/i.test(navigator.userAgent);
      return isIOS ? THREE.HalfFloatType : THREE.FloatType;
    }

    createAllFBO() {
      const type = this.getFloatType();
      const opts = {
        type,
        depthBuffer: false,
        stencilBuffer: false,
        minFilter: THREE.LinearFilter,
        magFilter: THREE.LinearFilter,
        wrapS: THREE.ClampToEdgeWrapping,
        wrapT: THREE.ClampToEdgeWrapping
      } as const;
      for (const key in this.fbos) {
        this.fbos[key] = new THREE.WebGLRenderTarget(this.fboSize.x, this.fboSize.y, opts);
      }
    }

    createShaderPass() {
      this.advection = new Advection({
        cellScale: this.cellScale,
        fboSize: this.fboSize,
        dt: this.options.dt,
        src: this.fbos.vel_0,
        dst: this.fbos.vel_1
      });
      this.externalForce = new ExternalForce({
        cellScale: this.cellScale,
        cursor_size: this.options.cursor_size,
        dst: this.fbos.vel_1
      });
      this.viscous = new Viscous({
        cellScale: this.cellScale,
        boundarySpace: this.boundarySpace,
        viscous: this.options.viscous,
        src: this.fbos.vel_1,
        dst: this.fbos.vel_viscous1,
        dst_: this.fbos.vel_viscous0,
        dt: this.options.dt
      });
      this.divergence = new Divergence({
        cellScale: this.cellScale,
        boundarySpace: this.boundarySpace,
        src: this.fbos.vel_viscous0,
        dst: this.fbos.div,
        dt: this.options.dt
      });
      this.poisson = new Poisson({
        cellScale: this.cellScale,
        boundarySpace: this.boundarySpace,
        src: this.fbos.div,
        dst: this.fbos.pressure_1,
        dst_: this.fbos.pressure_0
      });
      this.pressure = new Pressure({
        cellScale: this.cellScale,
        boundarySpace: this.boundarySpace,
        src_p: this.fbos.pressure_0,
        src_v: this.fbos.vel_viscous0,
        dst: this.fbos.vel_0,
        dt: this.options.dt
      });
    }

    calcSize() {
      const width = Math.max(1, Math.round(this.options.resolution * Common.width));
      const height = Math.max(1, Math.round(this.options.resolution * Common.height));
      this.cellScale.set(1 / width, 1 / height);
      this.fboSize.set(width, height);
    }

    resize() {
      this.calcSize();
      for (const key in this.fbos) {
        this.fbos[key]!.setSize(this.fboSize.x, this.fboSize.y);
      }
    }

    update() {
      if (this.options.isBounce) this.boundarySpace.set(0, 0);
      else this.boundarySpace.copy(this.cellScale);
      this.advection.update({ dt: this.options.dt, isBounce: this.options.isBounce, BFECC: this.options.BFECC });
      this.externalForce.update({
        cursor_size: this.options.cursor_size,
        mouse_force: this.options.mouse_force,
        cellScale: this.cellScale
      });
      let vel: any = this.fbos.vel_1;
      if (this.options.isViscous) {
        vel = this.viscous.update({
          viscous: this.options.viscous,
          iterations: this.options.iterations_viscous,
          dt: this.options.dt
        });
      }
      this.divergence.update({ vel });
      const pressure = this.poisson.update({ iterations: this.options.iterations_poisson });
      this.pressure.update({ vel, pressure });
    }
  }

  class Output {
    simulation: Simulation;
    scene: THREE.Scene;
    camera: THREE.Camera;
    output: THREE.Mesh;

    constructor() {
      this.simulation = new Simulation();
      this.scene = new THREE.Scene();
      this.camera = new THREE.Camera();
      this.output = new THREE.Mesh(
        new THREE.PlaneGeometry(2, 2),
        new THREE.RawShaderMaterial({
          vertexShader: face_vert,
          fragmentShader: color_frag,
          transparent: true,
          depthWrite: false,
          uniforms: {
            velocity: { value: this.simulation.fbos.vel_0!.texture },
            boundarySpace: { value: new THREE.Vector2() },
            palette: { value: paletteTex },
            bgColor: { value: bgVec4 }
          }
        })
      );
      this.scene.add(this.output);
    }

    resize() {
      this.simulation.resize();
    }

    render() {
      if (!Common.renderer) return;
      Common.renderer.setRenderTarget(null);
      Common.renderer.render(this.scene, this.camera);
    }

    update() {
      this.simulation.update();
      this.render();
    }
  }

  class WebGLManager implements LiquidEtherWebGL {
    props: any;
    output!: Output;
    autoDriver?: AutoDriver;
    lastUserInteraction = performance.now();
    running = false;
    private _loop = this.loop.bind(this);
    private _resize = this.resize.bind(this);
    private _onVisibility?: () => void;

    constructor(props: any) {
      this.props = props;
      Common.init(props.$wrapper);
      Mouse.init(props.$wrapper);
      Mouse.autoIntensity = props.autoIntensity;
      Mouse.takeoverDuration = props.takeoverDuration;
      Mouse.onInteract = () => {
        this.lastUserInteraction = performance.now();
        if (this.autoDriver) this.autoDriver.forceStop();
      };
      this.autoDriver = new AutoDriver(Mouse, this as any, {
        enabled: props.autoDemo,
        speed: props.autoSpeed,
        resumeDelay: props.autoResumeDelay,
        rampDuration: props.autoRampDuration
      });
      this.init();
      window.addEventListener('resize', this._resize);
      this._onVisibility = () => {
        const hidden = document.hidden;
        if (hidden) {
          this.pause();
        } else if (isVisibleRef.value) {
          this.start();
        }
      };
      document.addEventListener('visibilitychange', this._onVisibility);
    }

    init() {
      if (!Common.renderer) return;
      this.props.$wrapper.prepend(Common.renderer.domElement);
      this.output = new Output();
    }

    resize() {
      Common.resize();
      this.output.resize();
    }

    render() {
      if (this.autoDriver) this.autoDriver.update();
      Mouse.update();
      Common.update();
      this.output.update();
    }

    loop() {
      if (!this.running) return;
      this.render();
      rafRef.value = requestAnimationFrame(this._loop);
    }

    start() {
      if (this.running) return;
      this.running = true;
      this._loop();
    }

    pause() {
      this.running = false;
      if (rafRef.value) {
        cancelAnimationFrame(rafRef.value);
        rafRef.value = null;
      }
    }

    dispose() {
      try {
        window.removeEventListener('resize', this._resize);
        if (this._onVisibility) document.removeEventListener('visibilitychange', this._onVisibility);
        Mouse.dispose();
        if (Common.renderer) {
          const canvas = Common.renderer.domElement;
          if (canvas && canvas.parentNode) canvas.parentNode.removeChild(canvas);
          Common.renderer.dispose();
        }
      } catch {
        /* noop */
      }
    }
  }

  const container = mountRef.value;
  container.style.position = container.style.position || 'relative';
  container.style.overflow = container.style.overflow || 'hidden';

  const webgl = new WebGLManager({
    $wrapper: container,
    autoDemo: props.autoDemo,
    autoSpeed: props.autoSpeed,
    autoIntensity: props.autoIntensity,
    takeoverDuration: props.takeoverDuration,
    autoResumeDelay: props.autoResumeDelay,
    autoRampDuration: props.autoRampDuration
  });
  webglRef.value = webgl;

  const applyOptionsFromProps = () => {
    if (!webglRef.value) return;
    const sim = webglRef.value.output?.simulation;
    if (!sim) return;
    const prevRes = sim.options.resolution;
    Object.assign(sim.options, {
      mouse_force: props.mouseForce,
      cursor_size: props.cursorSize,
      isViscous: props.isViscous,
      viscous: props.viscous,
      iterations_viscous: props.iterationsViscous,
      iterations_poisson: props.iterationsPoisson,
      dt: props.dt,
      BFECC: props.BFECC,
      resolution: props.resolution,
      isBounce: props.isBounce
    });
    if (props.resolution !== prevRes) sim.resize();
  };
  applyOptionsFromProps();
  webgl.start();

  const io = new IntersectionObserver(
    entries => {
      const entry = entries[0];
      const isVisible = entry.isIntersecting && entry.intersectionRatio > 0;
      isVisibleRef.value = isVisible;
      if (!webglRef.value) return;
      if (isVisible && !document.hidden) {
        webglRef.value.start();
      } else {
        webglRef.value.pause();
      }
    },
    { threshold: [0, 0.01, 0.1] }
  );
  io.observe(container);
  intersectionObserverRef.value = io;

  const ro = new ResizeObserver(() => {
    if (!webglRef.value) return;
    if (resizeRafRef.value) cancelAnimationFrame(resizeRafRef.value);
    resizeRafRef.value = requestAnimationFrame(() => {
      if (!webglRef.value) return;
      webglRef.value.resize();
    });
  });
  ro.observe(container);
  resizeObserverRef.value = ro;
};

// Watchers for prop changes
watch(
  () => [
    props.mouseForce,
    props.cursorSize,
    props.isViscous,
    props.viscous,
    props.iterationsViscous,
    props.iterationsPoisson,
    props.dt,
    props.BFECC,
    props.resolution,
    props.isBounce,
    props.autoDemo,
    props.autoSpeed,
    props.autoIntensity,
    props.takeoverDuration,
    props.autoResumeDelay,
    props.autoRampDuration
  ],
  () => {
    const webgl = webglRef.value;
    if (!webgl) return;
    const sim = webgl.output?.simulation;
    if (!sim) return;
    const prevRes = sim.options.resolution;
    Object.assign(sim.options, {
      mouse_force: props.mouseForce,
      cursor_size: props.cursorSize,
      isViscous: props.isViscous,
      viscous: props.viscous,
      iterations_viscous: props.iterationsViscous,
      iterations_poisson: props.iterationsPoisson,
      dt: props.dt,
      BFECC: props.BFECC,
      resolution: props.resolution,
      isBounce: props.isBounce
    });
    if (webgl.autoDriver) {
      webgl.autoDriver.enabled = props.autoDemo;
      webgl.autoDriver.speed = props.autoSpeed;
      webgl.autoDriver.resumeDelay = props.autoResumeDelay;
      webgl.autoDriver.rampDurationMs = props.autoRampDuration * 1000;
      if (webgl.autoDriver.mouse) {
        webgl.autoDriver.mouse.autoIntensity = props.autoIntensity;
        webgl.autoDriver.mouse.takeoverDuration = props.takeoverDuration;
      }
    }
    if (props.resolution !== prevRes) sim.resize();
  }
);

onMounted(() => {
  initWebGL();
});

onUnmounted(() => {
  if (rafRef.value) cancelAnimationFrame(rafRef.value);
  if (resizeObserverRef.value) {
    try {
      resizeObserverRef.value.disconnect();
    } catch {
      /* noop */
    }
  }
  if (intersectionObserverRef.value) {
    try {
      intersectionObserverRef.value.disconnect();
    } catch {
      /* noop */
    }
  }
  if (webglRef.value) {
    webglRef.value.dispose();
  }
  webglRef.value = null;
});
</script>
