package org.openrndr.extra.objloader

import org.openrndr.draw.VertexBuffer
import org.openrndr.draw.vertexBuffer
import org.openrndr.draw.vertexFormat
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import java.io.File
import java.net.MalformedURLException
import java.net.URL
import kotlin.math.max
import kotlin.math.min

class Triangle(val positions: Array<Vector3> = emptyArray(),
               val normals: Array<Vector3> = emptyArray(),
               val textureCoords: Array<Vector2> = emptyArray()) {
    fun transform(t: Matrix44): Triangle {
        return Triangle(positions.map { (t * it.xyz1).div }.toTypedArray(), normals, textureCoords)
    }
}

class Box(val corner: Vector3, val width: Double, val height: Double, val depth: Double)

fun bounds(triangles: List<Triangle>): Box {
    var minX = Double.POSITIVE_INFINITY
    var minY = Double.POSITIVE_INFINITY
    var minZ = Double.POSITIVE_INFINITY

    var maxX = Double.NEGATIVE_INFINITY
    var maxY = Double.NEGATIVE_INFINITY
    var maxZ = Double.NEGATIVE_INFINITY

    triangles.forEach {
        it.positions.forEach {
            minX = min(minX, it.x)
            minY = min(minY, it.y)
            minZ = min(minZ, it.z)

            maxX = max(maxX, it.x)
            maxY = max(maxY, it.y)
            maxZ = max(maxZ, it.z)
        }
    }
    return Box(Vector3(minX, minY, minZ), maxX - minX, maxY - minY, maxZ - minZ)
}

fun List<Triangle>.vertexBuffer(): VertexBuffer {
    val vertexBuffer = vertexBuffer(objVertexFormat, size * 3)
    vertexBuffer.put {
        this@vertexBuffer.forEach {
            for (i in it.positions.indices) {
                write(it.positions[i])
                write(it.normals[i])
                write(Vector2.ZERO)
            }
        }
    }
    vertexBuffer.shadow.destroy()
    return vertexBuffer
}

fun loadOBJ(fileOrUrl: String): Map<String, List<Triangle>> {
    return try {
        val url = URL(fileOrUrl)
        loadOBJ(url)
    } catch (e: MalformedURLException) {
        loadOBJ(File(fileOrUrl))
    }
}

private val objVertexFormat = vertexFormat {
    position(3)
    normal(3)
    textureCoordinate(2)
}

fun loadOBJasVertexBuffer(fileOrUrl: String): VertexBuffer {
    return try {
        val url = URL(fileOrUrl)
        loadOBJasVertexBuffer(url)
    } catch (e: MalformedURLException) {
        loadOBJasVertexBuffer(File(fileOrUrl))
    }
}

fun loadOBJasVertexBuffer(url: URL): VertexBuffer = loadOBJasVertexBuffer(url.readText().split("\n"))
fun loadOBJasVertexBuffer(file: File): VertexBuffer = loadOBJasVertexBuffer(file.readLines())
fun loadOBJasVertexBuffer(lines: List<String>): VertexBuffer {
    val objects = loadOBJ(lines)
    val triangleCount = objects.values.sumBy { it.size }
    val vertexBuffer = vertexBuffer(objVertexFormat, triangleCount * 3)

    vertexBuffer.put {
        objects.entries.forEach {
            it.value.forEach {
                for (i in it.positions.indices) {
                    write(it.positions[i])
                    if (it.normals.isNotEmpty()) {
                        write(it.normals[i])
                    } else {
                        val d0 = it.positions[2] - it.positions[0]
                        val d1 = it.positions[1] - it.positions[0]
                        write(d0.normalized.cross(d1.normalized).normalized)
                    }
                    if (it.textureCoords.isNotEmpty()) {
                        write(it.textureCoords[i])
                    } else {
                        write(Vector2.ZERO)
                    }
                }
            }
        }
    }

    vertexBuffer.shadow.destroy()
    return vertexBuffer
}

fun loadOBJ(file: File) = loadOBJ(file.readLines())
fun loadOBJEx(file: File) = loadOBJEx(file.readLines())
fun loadOBJ(url: URL) = loadOBJ(url.readText().split("\n"))
fun loadOBJEx(url: URL) = loadOBJEx(url.readText().split("\n"))

class OBJData(val positions: List<Vector3>, val normals: List<Vector3>, val textureCoords: List<Vector2>)

fun loadOBJ(lines: List<String>): Map<String, List<Triangle>> = loadOBJEx(lines).second

fun loadOBJEx(lines: List<String>): Pair<OBJData, Map<String, List<Triangle>>> {
    val meshes = mutableMapOf<String, List<Triangle>>()
    val positions = mutableListOf<Vector3>()
    val normals = mutableListOf<Vector3>()
    val textureCoords = mutableListOf<Vector2>()
    var activeMesh = mutableListOf<Triangle>()

    lines.forEach { line ->
        if (line.isNotEmpty()) {
            val tokens = line.split(Regex("[ |\t]+")).map { it.trim() }.filter { it.isNotEmpty() }

            if (tokens.isNotEmpty()) {
                when (tokens[0]) {
                    "v" -> {
                        positions += Vector3(tokens[1].toDouble(), tokens[2].toDouble(), tokens[3].toDouble())
                    }
                    "vn" -> normals += Vector3(tokens[1].toDouble(), tokens[2].toDouble(), tokens[3].toDouble())
                    "vt" -> textureCoords += Vector2(tokens[1].toDouble(), tokens[2].toDouble())
                    "g" -> {
                        activeMesh = mutableListOf()
                        meshes[tokens.getOrNull(1) ?: "no-name-${meshes.size}"] = activeMesh
                    }
                    "f" -> {
                        val indices = tokens.subList(1, tokens.size).map { it.split("/") }.map {
                            it.map { it.toIntOrNull() }
                        }

                        for (i in 0 until indices.size - 2) {

                            val attributes = indices[0].size
                            val o = i * 2
                            val s = indices.size

                            val ps = if (attributes >= 1) arrayOf(
                                    indices[(0 + o) % s][0]?.let { positions[it - 1] } ?: Vector3.ZERO,
                                    indices[(1 + o) % s][0]?.let { positions[it - 1] } ?: Vector3.ZERO,
                                    indices[(2 + o) % s][0]?.let { positions[it - 1] } ?: Vector3.ZERO)
                            else
                                emptyArray()

                            val tcs = if (attributes >= 2) arrayOf(
                                    indices[(0 + o) % s][1]?.let { textureCoords[it - 1] } ?: Vector2.ZERO,
                                    indices[(1 + o) % s][1]?.let { textureCoords[it - 1] } ?: Vector2.ZERO,
                                    indices[(2 + o) % s][1]?.let { textureCoords[it - 1] } ?: Vector2.ZERO)
                            else
                                emptyArray()


                            val ns = if (attributes >= 3) arrayOf(
                                    indices[(0 + o) % s][2]?.let { normals[it - 1] } ?: Vector3.ZERO,
                                    indices[(1 + o) % s][2]?.let { normals[it - 1] } ?: Vector3.ZERO,
                                    indices[(2 + o) % s][2]?.let { normals[it - 1] } ?: Vector3.ZERO)
                            else
                                emptyArray()

                            activeMesh.add(Triangle(ps, ns, tcs))
                            if (meshes.isEmpty()) {
                                meshes["no-name"] = activeMesh
                            }
                        }
                    }
                }
            }
        }
    }
    return Pair(OBJData(positions, normals, textureCoords), meshes)
}