/* ──────────────────────────────────────────────────────────────────────────
 * Zeq · Nano-Zeqond GPU Field Kernel  —  Phase 3 (deep nano)
 *
 * A 2-D scalar field ϕ(x,y,t) evolving the HULYAS master-equation core
 *   □ϕ = ∇²ϕ − μ²ϕ − λϕ³
 * ENTIRELY on the GPU. Every cell of an N×N grid updates in parallel each
 * step, so cell-updates/second jumps from ~10⁶ (CPU, 1-D) toward 10⁸–10⁹.
 *
 *   • State in RGBA32F ping-pong textures (R = ϕ, G = π).
 *   • Velocity-Verlet in two fragment passes per step (2nd-order symplectic).
 *   • Periodic boundaries (REPEAT wrap, NEAREST sampling at texel centres).
 *   • dt pulses with the 1.287 Hz HulyaPulse (KO42: dt = dtSim·R(t)).
 *   • Self-renders ϕ to its canvas; throttled readback gives masterSum /
 *     energy / drift and the per-0.777 s zeqond prints.
 *
 * Mirrors the CPU engine API (advance / snapshot / prints / phase) so it drops
 * into the same UI. isSupported() gates on WebGL2 + float-render; callers fall
 * back to the CPU evolver when unavailable. Same physics, same KO42 ≤0.1%
 * drift — verified against a CPU reference before shipping.
 * ────────────────────────────────────────────────────────────────────────── */
(function (root, factory) {
  if (typeof module === 'object' && module.exports) module.exports = factory();
  else root.ZeqGpuField = factory();
}(typeof self !== 'undefined' ? self : this, function () {
  'use strict';

  const F_PULSE = 1.287, TAU_Z = 0.777, ALPHA = 1.29e-3;

  const VERT = `#version 300 es
in vec2 a_pos; out vec2 v_uv;
void main(){ v_uv = a_pos*0.5+0.5; gl_Position = vec4(a_pos,0.0,1.0); }`;

  const STEP = `#version 300 es
precision highp float;
uniform sampler2D u_state; uniform vec2 u_texel;
uniform float u_dt, u_mu2, u_lambda; uniform int u_pass;
in vec2 v_uv; out vec4 o_state;
float lap(vec2 uv){
  float c = texture(u_state, uv).r;
  float l = texture(u_state, uv - vec2(u_texel.x,0.0)).r;
  float r = texture(u_state, uv + vec2(u_texel.x,0.0)).r;
  float d = texture(u_state, uv - vec2(0.0,u_texel.y)).r;
  float u = texture(u_state, uv + vec2(0.0,u_texel.y)).r;
  return (l+r+d+u-4.0*c);
}
void main(){
  vec2 s = texture(u_state, v_uv).rg;
  float phi = s.r, pi = s.g;
  float acc = lap(v_uv) - u_mu2*phi - u_lambda*phi*phi*phi;
  if(u_pass==1){ float piHalf = pi + 0.5*u_dt*acc; float phiNew = phi + piHalf*u_dt; o_state = vec4(phiNew, piHalf, 0.0, 1.0); }
  else { float piNew = pi + 0.5*u_dt*acc; o_state = vec4(phi, piNew, 0.0, 1.0); }
}`;

  const SHOW = `#version 300 es
precision highp float;
uniform sampler2D u_state; uniform float u_scale;
in vec2 v_uv; out vec4 o_color;
void main(){
  float phi = texture(u_state, v_uv).r;
  float t = clamp(phi*u_scale*0.5+0.5, 0.0, 1.0);
  o_color = vec4(0.0, t, t*0.53, 1.0);
}`;

  function compile(gl, type, src){
    const sh = gl.createShader(type); gl.shaderSource(sh, src); gl.compileShader(sh);
    if(!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) throw new Error('shader: '+gl.getShaderInfoLog(sh));
    return sh;
  }
  function program(gl, fs){
    const p = gl.createProgram();
    gl.attachShader(p, compile(gl, gl.VERTEX_SHADER, VERT));
    gl.attachShader(p, compile(gl, gl.FRAGMENT_SHADER, fs));
    gl.bindAttribLocation(p, 0, 'a_pos'); gl.linkProgram(p);
    if(!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error('link: '+gl.getProgramInfoLog(p));
    return p;
  }

  class ZeqGpuField {
    static isSupported(canvas){
      // cache the probe result; never leak the throwaway context (frees it),
      // so repeated checks can't exhaust the live-WebGL-context limit.
      if(!canvas && ZeqGpuField._sup !== undefined) return ZeqGpuField._sup;
      try {
        const c = canvas || document.createElement('canvas');
        const gl = c.getContext('webgl2');
        if(!gl){ if(!canvas) ZeqGpuField._sup = false; return false; }
        const ok = !!gl.getExtension('EXT_color_buffer_float');
        if(!canvas){
          const lose = gl.getExtension('WEBGL_lose_context'); if(lose) lose.loseContext();
          ZeqGpuField._sup = ok;
        }
        return ok;
      } catch { if(!canvas) ZeqGpuField._sup = false; return false; }
    }

    constructor(canvas, opts = {}){
      this.canvas = canvas;
      const gl = canvas.getContext('webgl2', { antialias:false, preserveDrawingBuffer:false });
      if(!gl) throw new Error('WebGL2 unavailable');
      if(!gl.getExtension('EXT_color_buffer_float')) throw new Error('float render targets unavailable');
      this.gl = gl;

      this.N = opts.N || 256;                       // grid (N×N cells)
      this.mu2 = opts.mu2 != null ? opts.mu2 : 0.5;
      this.lambda = opts.lambda != null ? opts.lambda : 0.1;
      this.f = F_PULSE; this.tauZ = TAU_Z; this.alpha = ALPHA;
      this.phi0 = opts.phi0 != null ? opts.phi0 : 0;
      this.dtSim = opts.dtSim != null ? opts.dtSim : 0.05;       // 2-D CFL-safe
      this.simDtPerStep = opts.simDtPerStep != null ? opts.simDtPerStep : 31557600; // 1 yr/step default

      this.wallT = 0; this.simTime = 0; this.steps = 0;
      this.E0 = null; this.lastZeqond = 0; this.prints = [];
      this._read = { mean:0, energy:0, t:-1 };       // last readback cache

      this.progStep = program(gl, STEP);
      this.progShow = program(gl, SHOW);

      // full-screen quad
      this.quad = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, this.quad);
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);

      this._initTextures();
      this.seed(opts.seed);
      this._readback(true);   // baseline energy
    }

    _makeTex(){
      const gl = this.gl, t = gl.createTexture();
      gl.bindTexture(gl.TEXTURE_2D, t);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, this.N, this.N, 0, gl.RGBA, gl.FLOAT, null);
      return t;
    }

    _initTextures(){
      const gl = this.gl;
      this.texA = this._makeTex(); this.texB = this._makeTex();
      this.fboA = gl.createFramebuffer(); this.fboB = gl.createFramebuffer();
      gl.bindFramebuffer(gl.FRAMEBUFFER, this.fboA);
      gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texA, 0);
      gl.bindFramebuffer(gl.FRAMEBUFFER, this.fboB);
      gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texB, 0);
      gl.bindFramebuffer(gl.FRAMEBUFFER, null);
      this.cur = 'A';
    }

    seed(kind){
      const N = this.N, data = new Float32Array(N*N*4);
      for(let y=0;y<N;y++) for(let x=0;x<N;x++){
        const i = (y*N+x)*4;
        const dx = (x - N/2)/(N/7), dy = (y - N/2)/(N/7);
        const r2 = dx*dx + dy*dy;
        const phi = (kind==='noise') ? (Math.random()-0.5)*0.3 : 0.6*Math.exp(-r2);
        data[i]=phi; data[i+1]=0; data[i+2]=0; data[i+3]=1;
      }
      const gl = this.gl;
      for(const t of [this.texA, this.texB]){
        gl.bindTexture(gl.TEXTURE_2D, t);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, N, N, 0, gl.RGBA, gl.FLOAT, data);
      }
      this.cur='A'; this.E0=null;
    }

    _srcTex(){ return this.cur==='A' ? this.texA : this.texB; }
    _dstFbo(){ return this.cur==='A' ? this.fboB : this.fboA; }
    _flip(){ this.cur = this.cur==='A' ? 'B' : 'A'; }

    _pass(pass, dt){
      const gl = this.gl;
      gl.useProgram(this.progStep);
      gl.bindFramebuffer(gl.FRAMEBUFFER, this._dstFbo());
      gl.viewport(0,0,this.N,this.N);
      gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this._srcTex());
      gl.uniform1i(gl.getUniformLocation(this.progStep,'u_state'), 0);
      gl.uniform2f(gl.getUniformLocation(this.progStep,'u_texel'), 1/this.N, 1/this.N);
      gl.uniform1f(gl.getUniformLocation(this.progStep,'u_dt'), dt);
      gl.uniform1f(gl.getUniformLocation(this.progStep,'u_mu2'), this.mu2);
      gl.uniform1f(gl.getUniformLocation(this.progStep,'u_lambda'), this.lambda);
      gl.uniform1i(gl.getUniformLocation(this.progStep,'u_pass'), pass);
      this._drawQuad(this.progStep);
      this._flip();
    }

    _drawQuad(prog){
      const gl = this.gl;
      gl.bindBuffer(gl.ARRAY_BUFFER, this.quad);
      gl.enableVertexAttribArray(0);
      gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
      gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
    }

    /** One full velocity-Verlet step (two GPU passes). */
    step(){
      const dt = this.dtSim * (1 + this.alpha*Math.sin(2*Math.PI*this.f*this.wallT + this.phi0));
      this._pass(1, dt);
      this._pass(2, dt);
      this.steps++; this.simTime += this.simDtPerStep;
    }
    block(B){ for(let k=0;k<B;k++) this.step(); }

    /** Read the field back and update mean/energy (used for stats + prints). */
    _readback(force){
      const gl = this.gl, N = this.N;
      const buf = new Float32Array(N*N*4);
      gl.bindFramebuffer(gl.FRAMEBUFFER, this.cur==='A' ? this.fboA : this.fboB);
      gl.readPixels(0,0,N,N, gl.RGBA, gl.FLOAT, buf);
      gl.bindFramebuffer(gl.FRAMEBUFFER, null);
      let mean=0, E=0;
      for(let y=0;y<N;y++) for(let x=0;x<N;x++){
        const i=(y*N+x)*4, phi=buf[i], pi=buf[i+1];
        const xr=((x+1)%N), yu=((y+1)%N);
        const gx=buf[(y*N+xr)*4]-phi, gy=buf[(yu*N+x)*4]-phi;
        mean += phi;
        E += 0.5*pi*pi + 0.5*(gx*gx+gy*gy) + 0.5*this.mu2*phi*phi + 0.25*this.lambda*phi*phi*phi*phi;
      }
      const cells=N*N;
      this._read = { mean: mean/cells, energy: E/cells, t: this.wallT };
      if(this.E0==null || force) this.E0 = this._read.energy;
    }

    energy(){ return this._read.energy; }
    driftPct(){ if(this.E0==null || Math.abs(this.E0)<1e-12) return 0; return Math.abs((this._read.energy - this.E0)/this.E0)*100; }
    infoIntegrity(){ return 1/(1+Math.abs(this._read.energy)); }
    masterSum(){ return this._read.mean*this.infoIntegrity() + 0.5 + 0.1*Math.sin(2*Math.PI*this.f*this.wallT); }
    phase(){ return (this.wallT % this.tauZ)/this.tauZ; }

    /** Render ϕ to the visible canvas. */
    render(){
      const gl = this.gl;
      gl.useProgram(this.progShow);
      gl.bindFramebuffer(gl.FRAMEBUFFER, null);
      gl.viewport(0,0,this.canvas.width, this.canvas.height);
      gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this._srcTex());
      gl.uniform1i(gl.getUniformLocation(this.progShow,'u_state'), 0);
      gl.uniform1f(gl.getUniformLocation(this.progShow,'u_scale'), 1.4);
      this._drawQuad(this.progShow);
    }

    /** Advance wall time, run B steps on GPU, print on each zeqond boundary. */
    advance(dtWall, B){
      this.wallT += dtWall;
      this.block(B);
      const z = Math.floor(this.wallT / this.tauZ);
      let print = null;
      if(z > this.lastZeqond){
        this.lastZeqond = z; this._readback(false);
        print = {
          zeqond:z, wallT:this.wallT, simTime:this.simTime, steps:this.steps,
          masterSum:this.masterSum(), energy:this.energy(), driftPct:this.driftPct(),
          phase:this.phase(), cells:this.N*this.N,
        };
        this.prints.push(print); if(this.prints.length>512) this.prints.shift();
      }
      return print;
    }

    /** Throttled stats refresh (call ~5 Hz from the driver). */
    refreshStats(){ if(this.wallT - this._read.t > 0.18) this._readback(false); }

    snapshot(){
      return {
        wallT:this.wallT, simTime:this.simTime, steps:this.steps, cells:this.N*this.N, gridN:this.N,
        masterSum:this.masterSum(), energy:this.energy(), driftPct:this.driftPct(), phase:this.phase(),
        pulse:1 + this.alpha*Math.sin(2*Math.PI*this.f*this.wallT + this.phi0),
        cellUpdates: this.steps * this.N * this.N,
      };
    }

    dispose(){
      const gl = this.gl;
      [this.texA,this.texB].forEach(t=>gl.deleteTexture(t));
      [this.fboA,this.fboB].forEach(f=>gl.deleteFramebuffer(f));
      gl.deleteBuffer(this.quad); gl.deleteProgram(this.progStep); gl.deleteProgram(this.progShow);
    }
  }

  ZeqGpuField.F_PULSE = F_PULSE; ZeqGpuField.TAU_Z = TAU_Z;
  return ZeqGpuField;
}));
