import { Node } from '../'
import { BoundingBox } from '../../mesh/aabb'
import { Mesh } from '../../mesh'
import { vec3 } from 'gl-matrix'
import { addv3 } from '../../utility'

const LOD_SEPARATOR = 16

class TerrainNode extends Node {
  constructor (root, pos, level) {
    super(pos)

    this.root = root
    this.level = level

    this.mesh = null
  }

  updateLODMesh (gl) {
    // If this mesh has children (we're a branch), generate their meshes instead
    if (this.children.length) {
      let generated = 0
      for (let i in this.children) {
        if (generated >= this.root.genPerTick) break
        let child = this.children[i]
        if (!(child instanceof TerrainNode)) continue
        generated += this.children[i].updateLODMesh(gl)
      }
      return generated
    }

    // If we already have a mesh, we can skip
    if (this.mesh) return 0

    let VERTICES = this.root.resolution
    let count = VERTICES * VERTICES
    let vertices = new Array(count * 3)
    let normals = new Array(count * 3)
    let textureCoords = new Array(count * 2)
    let indices = new Array(6 * (VERTICES - 1) * (VERTICES - 1))
    let vertexPointer = 0
    let divisionLevel = Math.pow(2, this.level)

    // Create vertices dynamically
    for (let i = 0; i < VERTICES; i++) {
      for (let j = 0; j < VERTICES; j++) {
        let vertDivj = j / (VERTICES - 1)
        let vertDivi = i / (VERTICES - 1)

        // Calculate relative resolution
        let pj = vertDivj * this.root.width / divisionLevel
        let pi = vertDivi * this.root.height / divisionLevel

        // Generator takes meshes' absolute vertex position in the world when using height values

        vertices[vertexPointer * 3] = pj
        vertices[vertexPointer * 3 + 1] = this.root.generator.getHeightPlaneMesh(this, pj, pi, true)
        vertices[vertexPointer * 3 + 2] = pi
        let normal = this.root.generator.getNormalPlaneMesh(this, pj, pi, true)
        normals[vertexPointer * 3] = normal[0]
        normals[vertexPointer * 3 + 1] = normal[1]
        normals[vertexPointer * 3 + 2] = normal[2]
        textureCoords[vertexPointer * 2] = j / (VERTICES - 1)
        textureCoords[vertexPointer * 2 + 1] = i / (VERTICES - 1)
        vertexPointer++
      }
    }

    // Create indices dynamically
    let pointer = 0
    for (let gz = 0; gz < VERTICES - 1; gz++) {
      for (let gx = 0; gx < VERTICES - 1; gx++) {
        let topLeft = (gz * VERTICES) + gx
        let topRight = topLeft + 1
        let bottomLeft = ((gz + 1) * VERTICES) + gx
        let bottomRight = bottomLeft + 1
        indices[pointer++] = topLeft
        indices[pointer++] = bottomLeft
        indices[pointer++] = topRight
        indices[pointer++] = topRight
        indices[pointer++] = bottomLeft
        indices[pointer++] = bottomRight
      }
    }

    this.mesh = Mesh.construct(gl, vertices, indices, textureCoords, normals)
    this.mesh.material = this.root.material
    this.bounds = BoundingBox.fromMesh(this.mesh)

    // Reduces flickering when subdividing
    if (this.parent && this.parent.mesh) {
      this.parent.dispose(gl)
    }

    return 1
  }

  draw (gl, shader) {
    // Draw child nodes
    super.draw(gl, shader)
    if (!this.mesh) return
    // Set model transform matrix uniform
    const transformLocation = shader.getUniformLocation(gl, 'uModelMatrix')
    gl.uniformMatrix4fv(transformLocation, false, this.transform)

    this.mesh.prepare(gl, shader)
    this.mesh.draw(gl, shader)
    this.mesh.postdraw(gl, shader)
  }

  get generated () {
    return this.mesh != null
  }

  dispose (gl) {
    if (!this.mesh) return
    this.mesh.dispose(gl)
    this.mesh = null
  }

  merge (gl) {
    // Can't merge if we have nothing to merge!
    if (this.children.length === 0) return

    // Merge children and dispose their meshes
    for (let i in this.children) {
      this.children[i].merge(gl)
      this.children[i].dispose(gl)
    }

    // Delete all children, (hopefully) they will be garbage collected
    this.children = []
  }

  subdivide () {
    // Do not divide when we're already at the limit
    if (this.level === this.root.maxDetail) return
    let lv = this.level + 1

    let stepLeft = this.root.width / Math.pow(2, lv)
    let stepForward = this.root.height / Math.pow(2, lv)

    // Child nodes take relative positions because they are parented
    this.addChild(new TerrainNode(this.root, [0, 0, 0], lv))
    this.addChild(new TerrainNode(this.root, [stepLeft, 0, 0], lv))
    this.addChild(new TerrainNode(this.root, [0, 0, stepForward], lv))
    this.addChild(new TerrainNode(this.root, [stepLeft, 0, stepForward], lv))
  }

  // Divide and merge meshes depending on camera position and lodDistance
  update (gl, camera) {
    if (!this.bounds) return false
    // Get camera distance from the center of this mesh
    // TODO: use edges
    let distCamera = vec3.distance(camera.pos, addv3(this.bounds.center, this.absolutePosition))

    // Get LOD change distance based on current division level and a fixed distance
    let lodDistance = this.root.lodDistance - (Math.pow(2, this.level) * LOD_SEPARATOR)

    // If this node has children, either merge if too far away or update the children
    if (this.children.length) {
      // Merge the node if the node is further than lodDistance, plus a set offset
      if (distCamera > lodDistance + LOD_SEPARATOR && this.level >= 1) {
        this.merge(gl)
        return true
      }

      // Update the children
      let acted = false
      for (let i in this.children) {
        let child = this.children[i]
        acted = child.update(gl, camera)
      }
      return acted
    }

    // If the camera is close enough to this node and we still have room to divide, divide!
    if (distCamera < lodDistance && this.level < this.root.maxDetail) {
      this.subdivide()

      return true
    }

    return false
  }
}

class LODTerrain extends Node {
  constructor (pos, sWidth, sHeight, lodDistance = 1024, maxDetail = 4, resolution = 32, genPerTick = 4) {
    super(pos)

    this.width = sWidth
    this.height = sHeight

    // LOD parameters
    this.genPerTick = genPerTick
    this.lodDistance = lodDistance
    this.maxDetail = maxDetail
    this.resolution = resolution

    // Create four initial nodes
    this.addChild(new TerrainNode(this, pos, 1))
    this.addChild(new TerrainNode(this, [pos[0] + sWidth / 2, pos[1], pos[2]], 1))
    this.addChild(new TerrainNode(this, [pos[0], pos[1], pos[2] + sHeight / 2], 1))
    this.addChild(new TerrainNode(this, [pos[0] + sWidth / 2, pos[1], pos[2] + sHeight / 2], 1))
  }

  setGenerator (generator) {
    this.generator = generator
  }

  setMaterial (mat) {
    this.material = mat
  }

  // Update terrain meshes
  updateLODMesh (gl) {
    // Ensure only genPerTick mesh(es) is/are generated every tick
    let generated = 0
    for (let i in this.children) {
      if (generated >= this.genPerTick) break
      let child = this.children[i]
      if (!(child instanceof TerrainNode)) continue
      generated += this.children[i].updateLODMesh(gl)
    }
    return generated
  }

  // Divide and merge meshes depending on camera position and lodDistance
  update (gl, camera) {
    // Ensure only one mesh update per tick
    let acted = false
    for (let i in this.children) {
      if (acted) break
      let child = this.children[i]
      acted = child.update(gl, camera)
    }
    return acted
  }
}

export { LODTerrain }
