package me.eater.threedom.example

import me.eater.threedom.dom.Document
import me.eater.threedom.dom.IDocument
import me.eater.threedom.dom.PlainNode
import me.eater.threedom.dom.createNode
import me.eater.threedom.example.shader.Plain
import me.eater.threedom.example.vertex.Simple
import me.eater.threedom.gl.GL
import me.eater.threedom.gl.ShaderPreProcessor
import me.eater.threedom.gl.dom.PerspectiveCamera
import me.eater.threedom.gl.dom.TriMesh
import me.eater.threedom.gl.texture.Texture
import me.eater.threedom.gl.vertex.VertexArrayObject
import me.eater.threedom.gl.vertex.VertexBuffer
import me.eater.threedom.utils.joml.toFloat
import me.eater.threedom.utils.joml.translation
import me.eater.threedom.utils.joml.vec3
import org.joml.Vector2f
import org.joml.Vector3f
import org.lwjgl.glfw.GLFW.*
import org.lwjgl.glfw.GLFWErrorCallback
import org.lwjgl.opengl.GL.createCapabilities
import org.lwjgl.opengl.GL11.*
import org.lwjgl.system.MemoryStack.stackPush
import org.lwjgl.system.MemoryUtil.NULL
import java.time.Instant
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin

fun main() {
    GLFWErrorCallback.createPrint(System.err).set()

    if (!glfwInit()) {
        println("Unable to initialize GLFW")
        return
    }

    glfwDefaultWindowHints()
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4)
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 4)
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE)

    val window = glfwCreateWindow(300, 300, "Hello world", NULL, NULL)
    if (window == NULL) {
        println("Failed creating window")
        return
    }

    val stack = stackPush()
    val pWidth = stack.mallocInt(1)
    val pHeight = stack.mallocInt(1)

    // Get the window size passed to glfwCreateWindow
    glfwGetWindowSize(window, pWidth, pHeight)

    // Get the resolution of the primary monitor
    val vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor()) ?: return

    // Center the window
    glfwSetWindowPos(
        window,
        (vidmode.width() - pWidth.get(0)) / 2,
        (vidmode.height() - pHeight.get(0)) / 2
    )


    // Make the OpenGL context current
    glfwMakeContextCurrent(window)

    var width = pWidth.get(0)
    var height = pHeight.get(0)

    // Enable v-sync
    glfwSwapInterval(1)
    glfwShowWindow(window)
    createCapabilities()

    val doc: IDocument = Document()

    val triMesh = doc.createNode<TriMesh>()

    val preProcessor = ShaderPreProcessor.createDefault()

    val mesh = listOf(
        -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
        0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
        0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
        0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
        -0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
        -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,

        -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
        0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
        0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
        0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
        -0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
        -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,

        -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
        -0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
        -0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
        -0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f,
        -0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f,
        -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f,

        0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
        0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
        0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
        0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
        0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
        0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,

        -0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
        0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,
        0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
        0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
        -0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f,
        -0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f,

        -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
        0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
        0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
        0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
        -0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
        -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f
    )

    val buffer = mesh
        .chunked(6)
        .map {
            Simple().apply {
                position = vec3(it[0], it[1], it[2])
                normal = vec3(it[3], it[4], it[5])
            }
        }
        .let {
            VertexBuffer(it)
        }

    val VAO = VertexArrayObject {
        buffer(Simple::position)
        buffer(Simple::normal)
    }

    triMesh.mesh = VAO
    println("Loading mesh")
    println("Loading shader program")
    val plainShader = Plain.load(preProcessor)
    val lampShader = Plain.lamp(preProcessor)

    val texture = Texture()
    texture.load(Simple::class.java.getResourceAsStream("/textures/fujiwara.jpg").readAllBytes())
    plainShader.apply {
        objectColor = vec3(1.0, 0.5, 0.0)
        lightColor = vec3(1.0, 1.0, 1.0)
    }

    triMesh.use(lampShader)

    val empty = doc.createNode<PlainNode>()

    doc.addNode(empty)
    empty.addNode(triMesh)

    val around = mutableListOf<TriMesh>()

    val amount = 3
    val deg = (PI * 2) / amount
    for (i in 0 until amount) {
        val trimeshDup = triMesh.clone()
        trimeshDup.use(plainShader)
        around.add(trimeshDup)
        triMesh.addNode(trimeshDup)

        trimeshDup.model {
            translate(2 * sin(deg * i), 0.0, 2 * cos(deg * i))
        }
    }

    val camera = doc.createNode<PerspectiveCamera>()
    doc.addNode(camera)

    glfwSetFramebufferSizeCallback(window) { _, newWidth, newHeight ->
        glViewport(0, 0, newWidth, newHeight)

        camera.width = newWidth.toDouble()
        camera.height = newHeight.toDouble()
    }

    glfwSetKeyCallback(window) { _, key, _, action, _ ->
        if ((key == GLFW_KEY_Q || key == GLFW_KEY_ESCAPE) && action == GLFW_RELEASE)
            glfwSetWindowShouldClose(window, true)


        var z = 0.0
        var x = 0.0
        val speed = 0.1
        if (key == GLFW_KEY_UP && action != GLFW_RELEASE) {
            z += speed
        }

        if (key == GLFW_KEY_DOWN && action != GLFW_RELEASE) {
            z -= speed
        }

        if (key == GLFW_KEY_LEFT && action != GLFW_RELEASE) {
            x += speed
        }

        if (key == GLFW_KEY_RIGHT && action != GLFW_RELEASE) {
            x -= speed
        }

        if (x != 0.0 || z != 0.0) {
            camera.model {
                translateLocal(x, 0.0, z)
            }
        }
    }

    println("Starting render loop")
    glClearColor(0.3f, 0.2f, 0.2f, 1.0f)
    glEnable(GL_DEPTH_TEST)
    while (!glfwWindowShouldClose(window)) {
        glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT)

        empty.model {
            setTranslation(0.0, 0.0, sin(Instant.now().toEpochMilli().toDouble() / 1000) - 3)
        }

        triMesh.model {
            rotateY(0.005)
        }

        around.forEach {
            it.model {
                rotateY(-0.005)
            }
        }

        plainShader.lightPos = triMesh.absolute.translation.toFloat()
        plainShader.viewPos  = camera.absolute.translation.toFloat()
        doc.render(GL, camera)

        glfwSwapBuffers(window)
        glfwPollEvents()
    }
}

