(ns nol.core
  (:require [clojure.math])
  (:import [org.lwjgl.assimp Assimp AIFace AIMaterial AIMesh AIScene AIVector3D]
           [org.lwjgl.glfw GLFW GLFWErrorCallbackI GLFWKeyCallbackI]
           [org.lwjgl.opengl GL GL11 GL15 GL20 GL33 GL30 GL45]
           [org.lwjgl.system MemoryUtil MemoryStack]
           [org.joml Matrix4f]
           [java.nio FloatBuffer]))

(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)]
     (.put render-queue #(deliver result# (try (do ~@body)
                                               (catch Exception e# e#))))
     (let [r# (deref result#)]
       (if (instance? Exception r#)
         (throw r#)
         r#))))

(def vertex-shader-text (slurp "shaders/base.vert"))

(def fragment-shader-text (slurp "shaders/base.frag"))

(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))

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

(def vertices (float-array [0.0   1.0 0.0
                            -1.0 -1.0 0.0
                            1.0  -1.0 0.0]))

(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)

    (GLFW/glfwSwapBuffers window)))

(defn stop! []
  (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 300 #_592 "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)))))

    (let []
      (loop []
        (while (not (.isEmpty render-queue))
          ((.take render-queue)))
        (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))))

(comment
  (start!)
  (stop!)
  (def ^AIScene scene (load-model "assets/model.glb"))
  (.mNumMeshes scene) ;; 54
  (.mNumMaterials scene) ;; 17

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

  (with-gl
    (GL45/glNamedBufferSubData (:vbo @state) 0 ^floats (float-array [ 0.0   0.5 0.0
                                                                     -0.5  -0.5 0.0
                                                                      0.5  -0.5 0.0])))
  (with-gl
    (GL45/glNamedBufferSubData (:vbo @state) 0 ^floats (float-array [ 0.0   1.0 0.0
                                                                     -1.0  -1.0 0.0
                                                                      1.0  -1.0 0.0])))

  (def program (with-gl (let [vert (compile-shader GL20/GL_VERTEX_SHADER vertex-shader-text)
                              frag (compile-shader GL20/GL_FRAGMENT_SHADER fragment-shader-text)]
                          (link-program vert frag))))

  (with-gl (GL45/glVertexArrayVertexBuffer vao 0 vbo 0 12))

  (with-gl (GL45/glEnableVertexArrayAttrib vao 0)
           (GL45/glVertexArrayAttribFormat vao 0 3 GL45/GL_FLOAT false 0)
           (GL45/glVertexArrayAttribBinding vao 0 0))

  :-)
