;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL

(ns app.render-wasm.api.webgl
  "WebGL utilities for pixel capture and rendering"
  (:require
   [app.common.logging :as log]
   [app.common.render-wasm.wasm :as wasm]
   [promesa.core :as p]))

(defn get-webgl-context
  "Gets the WebGL context from the WASM module"
  []
  (when (wasm/live?)
    (let [gl-obj (unchecked-get wasm/internal-module "GL")]
      (when gl-obj
        ;; Get the current WebGL context from Emscripten
        ;; The GL object has a currentContext property that contains the context handle
        (let [current-ctx (.-currentContext ^js gl-obj)]
          (when current-ctx
            (.-GLctx ^js current-ctx)))))))

(defn create-webgl-texture-from-image
  "Creates a WebGL texture from an HTMLImageElement or ImageBitmap and returns the texture object"
  [gl image-element]
  (let [texture (.createTexture ^js gl)]
    (.bindTexture ^js gl (.-TEXTURE_2D ^js gl) texture)
    (.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_S ^js gl) (.-CLAMP_TO_EDGE ^js gl))
    (.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_T ^js gl) (.-CLAMP_TO_EDGE ^js gl))
    (.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MIN_FILTER ^js gl) (.-LINEAR ^js gl))
    (.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MAG_FILTER ^js gl) (.-LINEAR ^js gl))
    (.texImage2D ^js gl (.-TEXTURE_2D ^js gl) 0 (.-RGBA ^js gl) (.-RGBA ^js gl) (.-UNSIGNED_BYTE ^js gl) image-element)
    (.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil)
    texture))

;; FIXME: temporary function until we are able to keep the same <canvas> across pages.
(defn- draw-imagedata-to-webgl
  "Draws ImageData to a WebGL2 context by creating a texture"
  [gl image-data]
  (let [width (.-width ^js image-data)
        height (.-height ^js image-data)
        texture (.createTexture ^js gl)]
    ;; Bind texture and set parameters
    (.bindTexture ^js gl (.-TEXTURE_2D ^js gl) texture)
    (.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_S ^js gl) (.-CLAMP_TO_EDGE ^js gl))
    (.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_T ^js gl) (.-CLAMP_TO_EDGE ^js gl))
    (.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MIN_FILTER ^js gl) (.-LINEAR ^js gl))
    (.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MAG_FILTER ^js gl) (.-LINEAR ^js gl))
    (.texImage2D ^js gl (.-TEXTURE_2D ^js gl) 0 (.-RGBA ^js gl) (.-RGBA ^js gl) (.-UNSIGNED_BYTE ^js gl) image-data)

    ;; Set up viewport
    (.viewport ^js gl 0 0 width height)

    ;; Vertex & Fragment shaders
    ;; Since we are only calling this function once (on page switch), we don't need
    ;; to cache the compiled shaders somewhere else (cannot be reused in a differen context).
    (let [vertex-shader-source "#version 300 es
in vec2 a_position;
in vec2 a_texCoord;
out vec2 v_texCoord;
void main() {
  gl_Position = vec4(a_position, 0.0, 1.0);
  v_texCoord = a_texCoord;
}"
          fragment-shader-source "#version 300 es
precision highp float;
in vec2 v_texCoord;
uniform sampler2D u_texture;
out vec4 fragColor;
void main() {
  fragColor = texture(u_texture, v_texCoord);
}"
          vertex-shader (.createShader ^js gl (.-VERTEX_SHADER ^js gl))
          fragment-shader (.createShader ^js gl (.-FRAGMENT_SHADER ^js gl))
          program (.createProgram ^js gl)]
      (.shaderSource ^js gl vertex-shader vertex-shader-source)
      (.compileShader ^js gl vertex-shader)
      (when-not (.getShaderParameter ^js gl vertex-shader (.-COMPILE_STATUS ^js gl))
        (log/error :hint "Vertex shader compilation failed"
                   :log (.getShaderInfoLog ^js gl vertex-shader)))

      (.shaderSource ^js gl fragment-shader fragment-shader-source)
      (.compileShader ^js gl fragment-shader)
      (when-not (.getShaderParameter ^js gl fragment-shader (.-COMPILE_STATUS ^js gl))
        (log/error :hint "Fragment shader compilation failed"
                   :log (.getShaderInfoLog ^js gl fragment-shader)))

      (.attachShader ^js gl program vertex-shader)
      (.attachShader ^js gl program fragment-shader)
      (.linkProgram ^js gl program)

      (if (.getProgramParameter ^js gl program (.-LINK_STATUS ^js gl))
        (do
          (.useProgram ^js gl program)

          ;; Create full-screen quad vertices (normalized device coordinates)
          (let [position-location (.getAttribLocation ^js gl program "a_position")
                texcoord-location (.getAttribLocation ^js gl program "a_texCoord")
                position-buffer (.createBuffer ^js gl)
                texcoord-buffer (.createBuffer ^js gl)
                positions #js [-1.0 -1.0  1.0 -1.0  -1.0 1.0  -1.0 1.0  1.0 -1.0  1.0 1.0]
                texcoords #js [0.0 0.0  1.0 0.0  0.0 1.0  0.0 1.0  1.0 0.0  1.0 1.0]]
            ;; Set up position buffer
            (.bindBuffer ^js gl (.-ARRAY_BUFFER ^js gl) position-buffer)
            (.bufferData ^js gl (.-ARRAY_BUFFER ^js gl) (js/Float32Array. positions) (.-STATIC_DRAW ^js gl))
            (.enableVertexAttribArray ^js gl position-location)
            (.vertexAttribPointer ^js gl position-location 2 (.-FLOAT ^js gl) false 0 0)
            ;; Set up texcoord buffer
            (.bindBuffer ^js gl (.-ARRAY_BUFFER ^js gl) texcoord-buffer)
            (.bufferData ^js gl (.-ARRAY_BUFFER ^js gl) (js/Float32Array. texcoords) (.-STATIC_DRAW ^js gl))
            (.enableVertexAttribArray ^js gl texcoord-location)
            (.vertexAttribPointer ^js gl texcoord-location 2 (.-FLOAT ^js gl) false 0 0)
            ;; Set texture uniform
            (.activeTexture ^js gl (.-TEXTURE0 ^js gl))
            (.bindTexture ^js gl (.-TEXTURE_2D ^js gl) texture)
            (let [texture-location (.getUniformLocation ^js gl program "u_texture")]
              (.uniform1i ^js gl texture-location 0))

            ;; draw
            (.drawArrays ^js gl (.-TRIANGLES ^js gl) 0 6)

            ;; cleanup
            (.deleteBuffer ^js gl position-buffer)
            (.deleteBuffer ^js gl texcoord-buffer)
            (.deleteShader ^js gl vertex-shader)
            (.deleteShader ^js gl fragment-shader)
            (.deleteProgram ^js gl program)))
        (log/error :hint "Program linking failed"
                   :log (.getProgramInfoLog ^js gl program)))

      (.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil)
      (.deleteTexture ^js gl texture))))

(defn capture-canvas-snapshot
  "Captures the current viewport canvas as an `ImageBitmap` and stores it in
  `wasm/canvas-snapshot`. Unlike `canvas.toBlob` (which does a synchronous GPU
  readback plus PNG encoding on the main thread), `createImageBitmap` resolves
  asynchronously and stays on the GPU in accelerated browsers.

  Returns a promise resolving to the ImageBitmap (or nil)."
  []
  (if-let [^js canvas wasm/canvas]
    (-> (js/createImageBitmap canvas)
        (p/then (fn [^js bitmap]
                  (set! wasm/canvas-snapshot bitmap)
                  bitmap))
        (p/catch (fn [_] nil)))
    (p/resolved nil)))

(defn draw-thumbnail-to-canvas
  "Loads an image from `uri` and draws it stretched to fill the WebGL canvas.
   Returns a promise that resolves to true if drawn, false otherwise."
  [uri]
  (if (and uri wasm/canvas wasm/gl-context)
    (p/create
     (fn [resolve _reject]
       (let [img (js/Image.)]
         (set! (.-crossOrigin img) "anonymous")
         (set! (.-onload img)
               (fn []
                 (if wasm/gl-context
                   (let [gl     wasm/gl-context
                         width  (.-width wasm/canvas)
                         height (.-height wasm/canvas)
                         ;; Draw image to an offscreen 2D canvas, scaled to fill
                         canvas2d (js/OffscreenCanvas. width height)
                         ctx2d   (.getContext canvas2d "2d")]
                     (.drawImage ctx2d img 0 0 width height)
                     (let [image-data (.getImageData ctx2d 0 0 width height)]
                       (draw-imagedata-to-webgl gl image-data))
                     (resolve true))
                   (resolve false))))
         (set! (.-onerror img)
               (fn [_]
                 (resolve false)))
         (set! (.-src img) uri))))
    (p/resolved false)))
