(ns nol.core
  (:require
   [clojure.math])
  (:import
   [java.nio FloatBuffer IntBuffer]
   [org.lwjgl.assimp AIColor4D AIMaterial AIMesh AIScene AIString AITexture Assimp]
   [org.lwjgl.glfw GLFW GLFWKeyCallbackI]
   [org.lwjgl.opengl GL GL11 GL20 GL30 GL45]
   [org.lwjgl.stb STBImage]
   [org.joml Matrix4f]
   [org.lwjgl.system MemoryStack MemoryUtil]))

(def render-queue (java.util.concurrent.LinkedBlockingQueue.))

(def state (atom {:last-time (GLFW/glfwGetTime) :window nil}))

(def gl-thread (atom nil))

(defmacro with-gl [& body]
  `(let [result# (promise)]
     (when-not (and @gl-thread (.isAlive @gl-thread))
       (throw (RuntimeException. "GL thread is not running")))
     (.put render-queue #(deliver result# (try (do ~@body)
                                               (catch Exception e# e#))))
     (let [r# (deref result#)]
       (if (instance? Exception r#)
         (throw r#)
         r#))))

(defn- compile-shader [type source]
  (let [shader (GL20/glCreateShader type)]
    (GL20/glShaderSource shader ^String source)
    (GL20/glCompileShader shader)
    (when (zero? (GL20/glGetShaderi shader GL20/GL_COMPILE_STATUS))
      (throw (RuntimeException. (str "Shader compile error: " (GL20/glGetShaderInfoLog shader)))))
    shader))

(defn- link-program [vertex-shader fragment-shader]
  (let [program (GL20/glCreateProgram)]
    (GL20/glAttachShader program vertex-shader)
    (GL20/glAttachShader program fragment-shader)
    (GL20/glLinkProgram program)
    (when (zero? (GL20/glGetProgrami program GL20/GL_LINK_STATUS))
      (throw (RuntimeException. (str "Program link error: " (GL20/glGetProgramInfoLog program)))))
    program))

(def loaded-scenes (atom []))
(def uploaded-scenes (atom []))
(def shaders (atom nil))

(defn- update-loop [window stack data]
  (let [w-buf (.mallocInt stack 1)
        h-buf (.mallocInt stack 1)
        _     (GLFW/glfwGetFramebufferSize window w-buf h-buf)
        w     (.get w-buf 0)
        h     (.get h-buf 0)
        t (GLFW/glfwGetTime)]
    (GL11/glViewport 0 0 w h)
    (GL11/glClear GL11/GL_COLOR_BUFFER_BIT)

    ;; (swap! state assoc :last-time t)
    (try
      (when (and (seq @uploaded-scenes) @shaders)
        (doseq [scene @uploaded-scenes
                :let [{:keys [meshes gl-meshes gl-textures]} scene
                      {:keys [program uniforms]} @shaders]]
          (GL20/glUseProgram program)
          (doseq [m gl-meshes
                  :let [[r g b a] (:color (:mesh m))
                        tex-idx (:texture-idx (:mesh m))]]
            (GL20/glUniform4f (:color uniforms) r g b a)
            (let [buf (.mallocFloat stack 16)
                  m (Matrix4f.)]
              (.get m buf)
              (GL20/glUniformMatrix4fv (:mm uniforms) false buf)
              (GL20/glUniformMatrix4fv (:mv uniforms) false buf)
              (GL20/glUniformMatrix4fv (:mp uniforms) false buf))
            (GL30/glBindVertexArray (:vao m))
            (when tex-idx
              (let [tex-id (:gl-id (nth gl-textures tex-idx))]
                (GL45/glActiveTexture GL45/GL_TEXTURE0)
                (GL11/glBindTexture GL11/GL_TEXTURE_2D tex-id)
                (GL20/glUniform1i (:tex uniforms) 0)))
            (GL11/glDrawElements GL11/GL_TRIANGLES (:indices-count m) GL11/GL_UNSIGNED_INT 0))))
      (catch Exception e
        (println "Update Loop GL thread error:" e)))

    (GL30/glBindVertexArray 0)
    (GL20/glUseProgram 0)

    (GLFW/glfwSwapBuffers window)))

(defn stop! []
  (reset! uploaded-scenes [])
  (reset! shaders nil)
  (with-gl
    (GLFW/glfwSetWindowShouldClose
      (:window @state) true)))

(defn -main []
  (when-not (GLFW/glfwInit)
    (throw (RuntimeException. "Failed to init GLFW")))

  (GLFW/glfwWindowHint GLFW/GLFW_CONTEXT_VERSION_MAJOR 4)
  (GLFW/glfwWindowHint GLFW/GLFW_CONTEXT_VERSION_MINOR 5)
  (GLFW/glfwWindowHint GLFW/GLFW_OPENGL_PROFILE GLFW/GLFW_OPENGL_CORE_PROFILE)

  (let [window (GLFW/glfwCreateWindow 960 #_1900 300 #_1100 "NOL" MemoryUtil/NULL MemoryUtil/NULL)]
    (when (= window MemoryUtil/NULL)
      (GLFW/glfwTerminate)
      (throw (RuntimeException. "Failed to create window")))


    (GLFW/glfwMakeContextCurrent window)
    (GL/createCapabilities)
    (GLFW/glfwSwapInterval 1)

    (swap! state assoc :window window :should-close false)

    (GLFW/glfwSetKeyCallback window
      (reify GLFWKeyCallbackI
        (invoke [_ win key scancode action mods]
          (when (and (= key GLFW/GLFW_KEY_ESCAPE) (= action GLFW/GLFW_PRESS))
            (swap! state assoc :should-close true)))))

    (loop []
      (loop []
        (when-let [f (.poll render-queue)]
          (f)
          (recur)))
      (when-not (or (:should-close @state)
                    (GLFW/glfwWindowShouldClose window))
        (GLFW/glfwPollEvents)
        (let [stack (MemoryStack/stackPush)]
          (try
            (update-loop window stack {})
            (finally
              (MemoryStack/stackPop))))
        (recur)))

    (GLFW/glfwDestroyWindow window)
    (GLFW/glfwTerminate)))

(defn start! []
  (when (and @gl-thread (.isAlive @gl-thread))
    (throw (RuntimeException. "Already running")))
  (reset! gl-thread
    (doto (Thread. (fn []
                     (try
                       (-main)
                       (catch Exception e
                         (println "GL thread error:" e)))))
      (.setName "opengl-thread")
      (.setDaemon true)
      (.start))))

(defn extract-mesh [^AIScene scene idx]
  (let [meshes (.mMeshes scene)]
    (let [mesh (AIMesh/create (long (.get meshes idx)))
          mat-idx (.mMaterialIndex mesh)
          mat-ptr ^long (.get (.mMaterials scene) mat-idx)
          material (AIMaterial/create (long mat-ptr))
          mat-color (AIColor4D/create)
          _ (Assimp/aiGetMaterialColor material Assimp/AI_MATKEY_COLOR_DIFFUSE 0 0 ^AIColor4D mat-color)
          ^IntBuffer no-int nil
          ^FloatBuffer no-float nil
          path (AIString/create)
          _ (Assimp/aiGetMaterialTexture material Assimp/aiTextureType_DIFFUSE 0 path no-int no-int no-float no-int no-int no-int)
          data-path (.dataString path)
          verts (.mVertices mesh)
          n (.mNumVertices mesh)
          vert-arr (float-array (* 5 n))
          faces (.mFaces mesh)
          nf (.mNumFaces mesh)
          idx-arr (int-array (* 3 nf))
          uvs (.mTextureCoords mesh 0)]
      (dotimes [i n]
        (let [vert (.get verts i)
              uv (when uvs (.get uvs i))
              [^float u ^float v] (if uvs [(.x uv) (.y uv)] [0.0 0.0])]
          (aset vert-arr (+ (* i 5) 0) (.x vert))
          (aset vert-arr (+ (* i 5) 1) (.y vert))
          (aset vert-arr (+ (* i 5) 2) (.z vert))
          (aset vert-arr (+ (* i 5) 3) u)
          (aset vert-arr (+ (* i 5) 4) v)))
      (dotimes [i nf]
        (let [face (.get faces i)
              idxs (.mIndices face)]
          (aset idx-arr (+ (* i 3) 0) (.get idxs 0))
          (aset idx-arr (+ (* i 3) 1) (.get idxs 1))
          (aset idx-arr (+ (* i 3) 2) (.get idxs 2))))
      {:verts vert-arr :indices idx-arr :mat-idx (.mMaterialIndex mesh)
       :texture-idx (when (.startsWith data-path "*")
                      (parse-long (subs data-path 1)))
       :color [(.r mat-color) (.g mat-color) (.b mat-color) (.a mat-color)]})))

(defn load-model [path]
  (let [flags (bit-or Assimp/aiProcess_Triangulate
                      Assimp/aiProcess_FlipUVs)]
    (Assimp/aiImportFile path flags)))

(defn load-scene [path]
  (let [scene (load-model path)
        meshes (doall
                 (for [i (range (.mNumMeshes scene))]
                   (extract-mesh scene i)))
        textures (let [texs (.mTextures scene)]
                   (doall
                     (for [i (range (.mNumTextures scene))]
                       (let [ai-tex (AITexture/create (.get texs i))
                             buf (.pcDataCompressed ai-tex)
                             w (int-array 1)
                             h (int-array 1)
                             channels (int-array 1)]
                         {:data (STBImage/stbi_load_from_memory buf w h channels 4)
                          :w (aget w 0) :h (aget h 0)}))))]
    {:scene scene :meshes meshes :textures textures}))

(defn upload-scene! [scene]
  (let [gl-meshes
        (mapv
          #(let [vbo (GL45/glCreateBuffers)
                 ebo (GL45/glCreateBuffers)
                 vao (GL45/glCreateVertexArrays)]
             (GL45/glNamedBufferStorage vbo (:verts %)   GL45/GL_DYNAMIC_STORAGE_BIT)
             (GL45/glNamedBufferStorage ebo (:indices %) GL45/GL_DYNAMIC_STORAGE_BIT)
             (GL45/glVertexArrayElementBuffer vao ebo)
             (GL45/glVertexArrayVertexBuffer vao 0 vbo 0 (* 5 Float/BYTES))

             (GL45/glVertexArrayAttribFormat vao 0 3 GL11/GL_FLOAT false 0)
             (GL45/glVertexArrayAttribBinding vao 0 0)

             (GL45/glVertexArrayAttribFormat vao 1 2 GL11/GL_FLOAT false (* 3 Float/BYTES))
             (GL45/glVertexArrayAttribBinding vao 1 0)

             (GL45/glEnableVertexArrayAttrib vao 0)
             (GL45/glEnableVertexArrayAttrib vao 1)
             {:vbo vbo :vao vao :ebo ebo
              :mesh %
              :indices-count (alength (:indices %))})
          (:meshes scene))
        gl-textures
        (mapv
          #(let [w (:w %)
                 h (:h %)
                 tex-id (GL45/glCreateTextures GL11/GL_TEXTURE_2D)]
             (GL45/glTextureStorage2D tex-id 1 GL11/GL_RGBA8 w h)
             (GL45/glTextureSubImage2D tex-id 0 0 0 w h GL11/GL_RGBA GL11/GL_UNSIGNED_BYTE (:data %))
             (GL45/glTextureParameteri tex-id GL11/GL_TEXTURE_MIN_FILTER GL11/GL_LINEAR)
             (GL45/glTextureParameteri tex-id GL11/GL_TEXTURE_MAG_FILTER GL11/GL_LINEAR)
             (STBImage/stbi_image_free (:data %))
             (assoc % :gl-id tex-id))
          (:textures scene))]
    (assoc scene :gl-textures gl-textures :gl-meshes gl-meshes)))

(defn load-shaders! [vert-path frag-path]
  (let [vert (compile-shader GL20/GL_VERTEX_SHADER (slurp vert-path))
        frag (compile-shader GL20/GL_FRAGMENT_SHADER (slurp frag-path))
        program (link-program vert frag)]
    {:program program
     :uniforms {:color (GL20/glGetUniformLocation program "uColor")
                :tex (GL20/glGetUniformLocation program "texture_id")
                :mm (GL20/glGetUniformLocation program "uModel")
                :mv (GL20/glGetUniformLocation program "uView")
                :mp (GL20/glGetUniformLocation program "uProjection")}}))

(comment
  (start!)
  (stop!)

  (@loaded-scenes)

  (swap! loaded-scenes conj (load-scene "assets/model.glb"))
  (swap! uploaded-scenes conj (with-gl (upload-scene! (first @loaded-scenes))))
  (reset! shaders (with-gl (load-shaders! "shaders/base.vert" "shaders/base.frag")))

  (with-gl
    (GL11/glClearColor 0.392 0.584 0.929 1.0))
  (with-gl
    (GL11/glClearColor 0.0 0.0 0.0 1.0))

  :-)
