import { Mesh } from '../mesh'
import { Node, MeshInstance } from '../components'
import { addv3, mulv3 } from '../utility'

const FACE_VERTEX = [
  // Bottom
  [
    [1.0, 0.0, 0.0],
    [1.0, 0.0, 1.0],
    [0.0, 0.0, 1.0],
    [0.0, 0.0, 0.0],
    [0.0, -1.0, 0.0]
  ],
  // Top
  [
    [0.0, 1.0, 0.0],
    [0.0, 1.0, 1.0],
    [1.0, 1.0, 1.0],
    [1.0, 1.0, 0.0],
    [0.0, 1.0, 0.0]
  ],
  // Left
  [
    [0.0, 0.0, 1.0],
    [0.0, 1.0, 1.0],
    [0.0, 1.0, 0.0],
    [0.0, 0.0, 0.0],
    [-1.0, 0.0, 0.0]
  ],
  // Right
  [
    [1.0, 0.0, 0.0],
    [1.0, 1.0, 0.0],
    [1.0, 1.0, 1.0],
    [1.0, 0.0, 1.0],
    [1.0, 0.0, 0.0]
  ],
  // Front
  [
    [1.0, 0.0, 1.0],
    [1.0, 1.0, 1.0],
    [0.0, 1.0, 1.0],
    [0.0, 0.0, 1.0],
    [0.0, 0.0, 1.0]
  ],
  // Back
  [
    [0.0, 0.0, 0.0],
    [0.0, 1.0, 0.0],
    [1.0, 1.0, 0.0],
    [1.0, 0.0, 0.0],
    [0.0, 0.0, -1.0]
  ]
]

const FACE_BOTTOM = 0
const FACE_TOP = 1
const FACE_LEFT = 2
const FACE_RIGHT = 3
const FACE_FRONT = 4
const FACE_BACK = 5

class Voxel {
  constructor (id) {
    this.id = id
  }

  get solid () {
    return this.id !== 0
  }
}

const AIR_VOXEL = new Voxel(0)
const GROUND_VOXEL = new Voxel(1)

class VoxelChunk extends MeshInstance {
  constructor (origin, pos, size = 16) {
    super(null, [origin[0] + pos[0] * size, origin[1] + pos[1] * size, origin[2] + pos[2] * size])
    this.relativePos = pos
    this.size = size
    this.mesh = null

    // Voxel data
    this.data = {}

    // Set to true when this chunk mesh requires to be recreated
    this.dirty = true

    // Set to true when the generation has been finished
    this.generated = false
  }

  getVoxel (x, y, z) {
    if (this.parent) {
      let neighbor
      if (x < 0) {
        neighbor = this.parent.getChunk(this.relativePos[0] - 1, this.relativePos[1], this.relativePos[2])
        if (neighbor) {
          return neighbor.getVoxel(this.size + x, y, z)
        }
        return AIR_VOXEL
      } else if (x >= this.size) {
        neighbor = this.parent.getChunk(this.relativePos[0] + 1, this.relativePos[1], this.relativePos[2])
        if (neighbor) {
          return neighbor.getVoxel(x - this.size, y, z)
        }
        return AIR_VOXEL
      } else if (y < 0) {
        neighbor = this.parent.getChunk(this.relativePos[0], this.relativePos[1] - 1, this.relativePos[2])
        if (neighbor) {
          return neighbor.getVoxel(x, this.size + y, z)
        }
        return AIR_VOXEL
      } else if (y >= this.size) {
        neighbor = this.parent.getChunk(this.relativePos[0], this.relativePos[1] + 1, this.relativePos[2])
        if (neighbor) {
          return neighbor.getVoxel(x, y - this.size, z)
        }
        return AIR_VOXEL
      } else if (z < 0) {
        neighbor = this.parent.getChunk(this.relativePos[0], this.relativePos[1], this.relativePos[2] - 1)
        if (neighbor) {
          return neighbor.getVoxel(x, y, this.size + z)
        }
        return AIR_VOXEL
      } else if (z >= this.size) {
        neighbor = this.parent.getChunk(this.relativePos[0], this.relativePos[1], this.relativePos[2] + 1)
        if (neighbor) {
          return neighbor.getVoxel(x, y, z - this.size)
        }
        return AIR_VOXEL
      }
    }

    return this.data[x][z][y]
  }

  getVoxelv (v) {
    return this.getVoxel(v[0], v[1], v[2])
  }

  generate (generator) {
    this.data = {}
    for (let x = 0; x < this.size; x++) {
      if (!this.data[x]) this.data[x] = {}
      for (let z = 0; z < this.size; z++) {
        if (!this.data[x][z]) this.data[x][z] = {}
        let columnHeight = generator.getHeight(x + this.pos[0], z + this.pos[2])
        for (let y = 0; y < this.size; y++) {
          let solid = this.pos[1] + y < columnHeight
          this.data[x][z][y] = solid ? GROUND_VOXEL : AIR_VOXEL
        }
      }
    }

    this.generated = true

    // Make sure the parent knows that we have generated everything
    if (this.parent) {
      this.parent.chunksLeft--
      if (this.parent.chunksLeft <= 0) {
        this.parent.generated = true
      }
    }
  }

  // Programmatically generate a voxel face
  // Returns the position, normal and texture coordinates for each vertex in this face
  createFace (points, pos, face) {
    // Add the corresponding offsets for this face to the position
    let corners = [
      addv3(pos, FACE_VERTEX[face][0]), addv3(pos, FACE_VERTEX[face][1]),
      addv3(pos, FACE_VERTEX[face][2]), addv3(pos, FACE_VERTEX[face][3])
    ]

    // Select the normal for this face
    let normal = FACE_VERTEX[face][4]

    // Return the 6 vertices that make up this face (two triangles)
    // They're named points because this function returns not only vertices,
    // but corresponding texture coordinates and normals at the same time for convenience
    points.push([corners[0], normal, [0.0, 1.0]])
    points.push([corners[1], normal, [0.0, 0.0]])
    points.push([corners[2], normal, [1.0, 0.0]])
    points.push([corners[0], normal, [0.0, 1.0]])
    points.push([corners[2], normal, [1.0, 0.0]])
    points.push([corners[3], normal, [1.0, 1.0]])
  }

  createMesh (gl) {
    // Makes sure the createMesh function is not called again while it is generating
    this.dirty = false

    // If there is no generated chunk, we have nothing to base a mesh off of
    if (!this.generated) return false

    // If there already exists a mesh, dispose of it
    if (this.mesh) {
      this.mesh.dispose(gl)
    }

    // Array of vertices with texture positions and normals
    let points = []

    // Generate face quads for each voxel in the chunk
    for (let x = 0; x < this.size; x++) {
      for (let y = 0; y < this.size; y++) {
        for (let z = 0; z < this.size; z++) {
          let cellPos = [x, y, z]
          if (!this.getVoxel(x, y, z).solid) continue

          if (!this.getVoxel(x, y - 1, z).solid) {
            this.createFace(points, cellPos, FACE_BOTTOM)
          }

          if (!this.getVoxel(x, y + 1, z).solid) {
            this.createFace(points, cellPos, FACE_TOP)
          }

          if (!this.getVoxel(x - 1, y, z).solid) {
            this.createFace(points, cellPos, FACE_LEFT)
          }

          if (!this.getVoxel(x + 1, y, z).solid) {
            this.createFace(points, cellPos, FACE_RIGHT)
          }

          if (!this.getVoxel(x, y, z + 1).solid) {
            this.createFace(points, cellPos, FACE_FRONT)
          }

          if (!this.getVoxel(x, y, z - 1).solid) {
            this.createFace(points, cellPos, FACE_BACK)
          }
        }
      }
    }

    // Do not create a mesh when there are no faces in this chunk
    if (points.length === 0) {
      return false
    }

    // Flatten the points array to three separate arrays
    let vertices = []
    let normals = []
    let uvs = []
    for (let i in points) {
      let vert = points[i]
      vertices.push(vert[0][0])
      vertices.push(vert[0][1])
      vertices.push(vert[0][2])
      normals.push(vert[1][0])
      normals.push(vert[1][1])
      normals.push(vert[1][2])
      uvs.push(vert[2][0])
      uvs.push(vert[2][1])
    }

    // Create a new mesh without an element array buffer (TODO maybe?)
    this.mesh = Mesh.construct(gl, vertices, null, uvs, normals)

    if (this.material) this.mesh.material = this.material

    return true
  }

  update (gl, dt) {
    if (this.dirty) return this.createMesh(gl)
    return false
  }

  destroy (gl) {
    this.generated = false
    this.mesh && this.mesh.dispose(gl)
    this.data = {}
  }
}

class VoxelMapBlock extends Node {
  constructor (pos, chunkSize = 16, size = 8) {
    super(pos)
    this.chunkSize = chunkSize
    this.size = size
    this.chunks = {}
    this.generated = false
    this.chunksLeft = size * size * size
  }

  getChunk (x, y, z) {
    if (x < 0 || x >= this.size || y < 0 || y >= this.size || z < 0 || z >= this.size) return null
    return this.chunks[x][z][y]
  }

  getChunkv (v) {
    return this.getChunk(v[0], v[1], v[2])
  }

  generate (generator) {
    this.chunks = {}
    for (let x = 0; x < this.size; x++) {
      if (!this.chunks[x]) this.chunks[x] = {}
      for (let z = 0; z < this.size; z++) {
        if (!this.chunks[x][z]) this.chunks[x][z] = {}
        for (let y = 0; y < this.size; y++) {
          if (this.chunks[x][z][y]) continue
          let chunk = new VoxelChunk(this.pos, [x, y, z], this.chunkSize)
          this.chunks[x][z][y] = chunk
          this.addChild(chunk)
          generator.pushChunk(chunk)
        }
      }
    }
  }

  destroy (gl) {
    this.generated = false
    for (let i in this.children) {
      let ch = this.children[i]
      if (!(ch instanceof VoxelChunk)) continue
      ch.destroy(gl)
    }
    this.children = []
    this.chunks = {}
  }

  update (gl, dt) {
    if (!this.generated) return false
    for (let i in this.children) {
      let ch = this.children[i]
      if (!(ch instanceof VoxelChunk)) continue
      if (ch.update(gl, dt)) return true
    }
  }
}

class VoxelWorld extends Node {
  constructor (generator, renderDistance = 2, worldSize = 64, blockSize = 8, chunkSize = 16) {
    super([0.0, 0.0, 0.0])
    this.generator = generator
    this.renderDistance = renderDistance
    this.generator = generator
    this.worldSize = worldSize
    this.blockSize = blockSize
    this.chunkSize = chunkSize
    this.blocks = {}
    this.mapblockqueue = []
  }

  queueMapblock (pos) {
    this.mapblockqueue.push(pos)
  }

  getBlock (x, y, z) {
    if (!this.blocks[x] || !this.blocks[x][z] || !this.blocks[x][z][y]) return null
    return this.blocks[x][z][y]
  }

  update (gl, cam, dt) {
    // Generate queued mapblocks before adding new ones to queue
    if (this.mapblockqueue.length) {
      let leftover = []
      let generated = false
      for (let i in this.mapblockqueue) {
        let pos = this.mapblockqueue[i]
        let check = this.getBlock(pos[0], pos[1], pos[2])

        if (generated || check) {
          leftover.push(pos)
          continue
        }

        let absPos = mulv3(pos, this.blockSize * this.chunkSize)
        let block = new VoxelMapBlock(absPos, this.chunkSize, this.blockSize)
        block.generate(this.generator)

        if (!this.blocks[pos[0]]) this.blocks[pos[0]] = {}
        if (!this.blocks[pos[0]][pos[2]]) this.blocks[pos[0]][pos[2]] = {}
        if (!this.blocks[pos[0]][pos[2]][pos[1]]) this.blocks[pos[0]][pos[2]][pos[1]] = {}

        this.blocks[pos[0]][pos[2]][pos[1]] = block
        this.addChild(block)
        generated = true
      }
      this.mapblockqueue = leftover
      return
    }

    // Calculate a fixed grid
    let total = this.blockSize * this.chunkSize
    let slgrid = [Math.floor(cam.pos[0] / total), Math.floor(cam.pos[1] / total), Math.floor(cam.pos[2] / total)]

    for (let x = slgrid[0] - this.renderDistance / 2; x < slgrid[0] + this.renderDistance / 2; x++) {
      for (let z = slgrid[2] - this.renderDistance / 2; z < slgrid[2] + this.renderDistance / 2; z++) {
        for (let y = slgrid[1] - this.renderDistance / 2; y < slgrid[1] + this.renderDistance / 2; y++) {
          if (this.getBlock(x, y, z)) continue
          this.queueMapblock([x, y, z])
        }
      }
    }

    for (let i in this.children) {
      if (this.children[i].update(gl, dt)) break
    }
  }
}

class VoxelGenerator {
  constructor (noise, material, groundHeight = 64) {
    this.material = material
    this.noise = noise
    this.groundHeight = groundHeight
    this.generateQueue = []
  }

  // Push a chunk into the generation queue
  pushChunk (chunk) {
    this.generateQueue.push(chunk)
  }

  // Get chunk height
  getHeight (x, z) {
    return this.noise.getHeight(x, z) + this.groundHeight
  }

  // Generate chunks in the queue
  update (dt) {
    for (let i in this.generateQueue) {
      let chunk = this.generateQueue[i]
      if (chunk.generated) continue

      chunk.material = this.material
      chunk.generate(this)
    }
  }
}

export { VoxelGenerator, Voxel, VoxelChunk, VoxelMapBlock, VoxelWorld }
