/* WebGL2 engine: compiles the uber shader, owns the render loop,
   exposes deterministic renderAt(phase) for exporters. */

var Engine = (function () {
  var canvas, gl, program, uniforms = {};
  var playing = true;
  var suspended = false;
  var loopT = 0;            // seconds into current loop
  var lastTick = 0;
  var fps = 60, fpsAcc = 0, fpsN = 0, fpsCb = null;
  var getParams = null;     // injected: () => P

  var UNIFORM_NAMES = [
    "u_res", "u_phase", "u_seed", "u_mode",
    "u_c1", "u_c2", "u_c3", "u_c4", "u_bg",
    "u_hue", "u_sat", "u_exposure", "u_contrast",
    "u_scale", "u_complex", "u_warp", "u_flow", "u_stretch",
    "u_light", "u_gloss", "u_lightAngle", "u_irid", "u_glow",
    "u_grain", "u_cell", "u_lines", "u_ca", "u_vig", "u_soft",
    "u_travel"
  ];

  function compile(type, src) {
    var sh = gl.createShader(type);
    gl.shaderSource(sh, src);
    gl.compileShader(sh);
    if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
      throw new Error("Shader compile error:\n" + gl.getShaderInfoLog(sh));
    }
    return sh;
  }

  function init(canvasEl, paramsGetter) {
    canvas = canvasEl;
    getParams = paramsGetter;
    gl = canvas.getContext("webgl2", {
      antialias: false,
      preserveDrawingBuffer: true,
      powerPreference: "high-performance"
    });
    if (!gl) throw new Error("WebGL2 not available");

    program = gl.createProgram();
    gl.attachShader(program, compile(gl.VERTEX_SHADER, VERT_SRC));
    gl.attachShader(program, compile(gl.FRAGMENT_SHADER, FRAG_SRC));
    gl.linkProgram(program);
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
      throw new Error("Program link error:\n" + gl.getProgramInfoLog(program));
    }
    gl.useProgram(program);

    var buf = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buf);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
    gl.enableVertexAttribArray(0);
    gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);

    UNIFORM_NAMES.forEach(function (n) {
      uniforms[n] = gl.getUniformLocation(program, n);
    });

    lastTick = performance.now();
    requestAnimationFrame(tick);
  }

  function setSize(w, h) {
    canvas.width = w;
    canvas.height = h;
    gl.viewport(0, 0, w, h);
  }

  function pushUniforms(P, phase) {
    gl.uniform2f(uniforms.u_res, canvas.width, canvas.height);
    gl.uniform1f(uniforms.u_phase, phase);
    gl.uniform1f(uniforms.u_seed, P.seed);
    gl.uniform1i(uniforms.u_mode, P.mode);

    gl.uniform3fv(uniforms.u_c1, hexToRgb01(P.c1));
    gl.uniform3fv(uniforms.u_c2, hexToRgb01(P.c2));
    gl.uniform3fv(uniforms.u_c3, hexToRgb01(P.c3));
    gl.uniform3fv(uniforms.u_c4, hexToRgb01(P.c4));
    gl.uniform3fv(uniforms.u_bg, hexToRgb01(P.bg));

    gl.uniform1f(uniforms.u_hue, P.hue);
    gl.uniform1f(uniforms.u_sat, P.sat);
    gl.uniform1f(uniforms.u_exposure, P.exposure);
    gl.uniform1f(uniforms.u_contrast, P.contrast);

    gl.uniform1f(uniforms.u_scale, P.scale);
    gl.uniform1f(uniforms.u_complex, P.complex);
    gl.uniform1f(uniforms.u_warp, P.warp);
    gl.uniform1f(uniforms.u_flow, P.flow);
    gl.uniform1f(uniforms.u_stretch, P.stretch);

    gl.uniform1f(uniforms.u_light, P.light);
    gl.uniform1f(uniforms.u_gloss, P.gloss);
    gl.uniform1f(uniforms.u_lightAngle, P.lightAngle);
    gl.uniform1f(uniforms.u_irid, P.irid);
    gl.uniform1f(uniforms.u_glow, P.glow);

    gl.uniform1f(uniforms.u_grain, P.grain);
    gl.uniform1f(uniforms.u_cell, P.cell);
    gl.uniform1f(uniforms.u_lines, P.lines);
    gl.uniform1f(uniforms.u_ca, P.ca);
    gl.uniform1f(uniforms.u_vig, P.vig);
    gl.uniform1f(uniforms.u_soft, P.soft);

    gl.uniform1f(uniforms.u_travel, P.travel);
  }

  function renderAt(phase) {
    var P = getParams();
    pushUniforms(P, phase);
    gl.drawArrays(gl.TRIANGLES, 0, 3);
  }

  function currentPhase() {
    var P = getParams();
    return (loopT / P.loop) % 1;
  }

  function tick(now) {
    if (suspended) return;

    var dt = Math.min((now - lastTick) / 1000, 0.1);
    lastTick = now;

    if (playing) {
      var P = getParams();
      loopT = (loopT + dt) % P.loop;
    }
    renderAt(currentPhase());

    fpsAcc += dt; fpsN++;
    if (fpsAcc >= 0.5) {
      fps = Math.round(fpsN / fpsAcc);
      fpsAcc = 0; fpsN = 0;
      if (fpsCb) fpsCb(fps);
    }
    requestAnimationFrame(tick);
  }

  function readPixels() {
    var w = canvas.width, h = canvas.height;
    var buf = new Uint8Array(w * h * 4);
    gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, buf);
    /* flip vertically: GL origin is bottom-left */
    var flipped = new Uint8Array(w * h * 4);
    var row = w * 4;
    for (var y = 0; y < h; y++) {
      flipped.set(buf.subarray(y * row, (y + 1) * row), (h - 1 - y) * row);
    }
    return flipped;
  }

  return {
    init: init,
    setSize: setSize,
    renderAt: renderAt,
    readPixels: readPixels,
    currentPhase: currentPhase,
    resetTime: function () { loopT = 0; },
    setLoopTime: function (t) { loopT = t; },
    suspend: function () { suspended = true; },
    resume: function () {
      suspended = false;
      lastTick = performance.now();
      requestAnimationFrame(tick);
    },
    setPlaying: function (v) { playing = v; lastTick = performance.now(); },
    isPlaying: function () { return playing; },
    onFps: function (cb) { fpsCb = cb; },
    canvas: function () { return canvas; },
    size: function () { return [canvas.width, canvas.height]; }
  };
})();
