import { Node } from '../'
import { Mesh } from '../../mesh'
import { Texture } from '../../mesh/material'
import Screen from '../../screen'

class WaterFBOs {
  constructor (reflectionWidth = 320, reflectionHeight = 180, refractionWidth = 1280, refractionHeight = 720) {
    this.reflectionHeight = reflectionHeight
    this.reflectionWidth = reflectionWidth
    this.refractionHeight = refractionHeight
    this.refractionWidth = refractionWidth
  }

  initialize (gl) {
    this.initReflectionFrameBuffer(gl)
    this.initRefractionFrameBuffer(gl)
  }

  initReflectionFrameBuffer (gl) {
    this.reflectionFrameBuffer = this.createFrameBuffer(gl)
    this.reflectionTexture = this.createTextureAttachment(gl, this.reflectionWidth, this.reflectionHeight)
    this.reflectionDepthBuffer = this.createDepthBufferAttachment(gl, this.reflectionWidth, this.reflectionHeight)
    this.unbindFrameBuffer(gl)
  }

  initRefractionFrameBuffer (gl) {
    this.refractionFrameBuffer = this.createFrameBuffer(gl)
    this.refractionTexture = this.createTextureAttachment(gl, this.refractionWidth, this.refractionHeight)
    this.refractionDepthTexture = this.createDepthTextureAttachment(gl, this.refractionWidth, this.refractionHeight)
    this.unbindFrameBuffer(gl)
  }

  createFrameBuffer (gl) {
    // generate frame buffer
    let frameBuffer = gl.createFramebuffer()
    // create the framebuffer
    gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer)
    // indicate that we will always render to color attachment 0
    // gl.drawBuffers.drawBuffersWEBGL([gl.drawBuffers.COLOR_ATTACHMENT0_WEBGL])
    return frameBuffer
  }

  unbindFrameBuffer (gl) {
    gl.bindFramebuffer(gl.FRAMEBUFFER, null)
    gl.viewport(0, 0, Screen.width, Screen.height)
  }

  bindFrameBuffer (gl, frameBuffer, width, height) {
    gl.bindTexture(gl.TEXTURE_2D, null) // To make sure the texture isn't bound
    gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer)
    gl.viewport(0, 0, width, height)
  }

  // call before rendering to this FBO
  bindReflectionFrameBuffer (gl) {
    this.bindFrameBuffer(gl, this.reflectionFrameBuffer, this.reflectionWidth, this.reflectionHeight)
  }

  // call before rendering to this FBO
  bindRefractionFrameBuffer (gl) {
    this.bindFrameBuffer(gl, this.refractionFrameBuffer, this.refractionWidth, this.refractionHeight)
  }

  createTextureAttachment (gl, width, height) {
    let texture = gl.createTexture()
    gl.bindTexture(gl.TEXTURE_2D, texture)
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, width, height, 0, gl.RGB, gl.UNSIGNED_BYTE, null)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0)
    return texture
  }

  createDepthTextureAttachment (gl, width, height) {
    let texture = gl.createTexture()
    gl.bindTexture(gl.TEXTURE_2D, texture)
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT, width, height, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_SHORT, null)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, texture, 0)
    return texture
  }

  createDepthBufferAttachment (gl, width, height) {
    let depthBuffer = gl.createRenderbuffer()
    gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer)
    gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height)
    gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer)
    return depthBuffer
  }
}

class WaterTile extends Node {
  constructor (pos, scale) {
    super(pos, [scale, 0.0, scale])
  }
}

class WaterRenderer {
  constructor (reflectivity = 0.1, tint = [0.0, 0.3, 0.5, 0.2]) {
    this.fbos = new WaterFBOs()

    this.reflectivity = reflectivity
    this.tint = tint
    this.phase = 1.0

    this.shininess = 0.7
    this.shineDamper = 20.0
    this.distortionStrength = 0.02
    this.waveSpeed = 0.001
  }

  async useDUDVMap (gl, file) {
    this.dudvMap = await Texture.fromFile(gl, file + '.png', false, gl.LINEAR)
  }

  async useNormalMap (gl, file) {
    this.normalMap = await Texture.fromFile(gl, file + '.png', false, gl.LINEAR)
  }

  reflect (gl, cam, render) {
    this.fbos.bindReflectionFrameBuffer(gl)
    let dist = 2 * cam.pos[1]
    cam.pos[1] -= dist
    cam.rotation[1] *= -1
    cam.updateTransform()
    render(gl)
    this.fbos.unbindFrameBuffer(gl)
    cam.pos[1] += dist
    cam.rotation[1] *= -1
    cam.updateTransform()
  }

  refract (gl, cam, render) {
    this.fbos.bindRefractionFrameBuffer(gl)
    render(gl)
    this.fbos.unbindFrameBuffer(gl)
  }

  async initialize (game) {
    this.shader = await game.shaders.createShaderFromFiles(game.gl, 'water', false)
    this.mesh = Mesh.constructFromVertices(game.gl, [-1, -1, -1, 1, 1, -1, 1, -1, -1, 1, 1, 1], 2)
    this.fbos.initialize(game.gl)
  }

  prepare (gl, cam, sun) {
    const camPosLoc = this.shader.getUniformLocation(gl, 'uCameraPosition')
    const refrLoc = this.shader.getUniformLocation(gl, 'refractionTexture')
    const reflLoc = this.shader.getUniformLocation(gl, 'reflectionTexture')
    const depthLoc = this.shader.getUniformLocation(gl, 'depthMap')
    const dudvLoc = this.shader.getUniformLocation(gl, 'dudvMap')
    const normalLoc = this.shader.getUniformLocation(gl, 'normalMap')

    const reflectivityLoc = this.shader.getUniformLocation(gl, 'uReflectivity')
    const tintLoc = this.shader.getUniformLocation(gl, 'uTint')
    const phaseLoc = this.shader.getUniformLocation(gl, 'uDUDVOffset')

    const shininessLoc = this.shader.getUniformLocation(gl, 'uShine')
    const damperLoc = this.shader.getUniformLocation(gl, 'uShineDamper')
    const distortLoc = this.shader.getUniformLocation(gl, 'uDistortionStrength')

    const farLoc = this.shader.getUniformLocation(gl, 'uFarPlane')
    const nearLoc = this.shader.getUniformLocation(gl, 'uNearPlane')

    const sunPosLoc = this.shader.getUniformLocation(gl, 'uLightPosition')
    const sunColorLoc = this.shader.getUniformLocation(gl, 'uLightColor')

    gl.uniform4fv(tintLoc, this.tint)
    gl.uniform3fv(camPosLoc, cam.pos)
    gl.uniform3fv(sunPosLoc, sun.pos)
    gl.uniform3fv(sunColorLoc, sun.color)
    gl.uniform1f(reflectivityLoc, this.reflectivity)
    gl.uniform1f(phaseLoc, this.phase)
    gl.uniform1f(shininessLoc, this.shininess)
    gl.uniform1f(damperLoc, this.shineDamper)
    gl.uniform1f(distortLoc, this.distortionStrength)
    gl.uniform1f(nearLoc, cam.nearPlane)
    gl.uniform1f(farLoc, cam.farPlane)
    gl.uniform1i(reflLoc, 0)
    gl.uniform1i(refrLoc, 1)
    gl.uniform1i(dudvLoc, 2)
    gl.uniform1i(normalLoc, 3)
    gl.uniform1i(depthLoc, 4)

    gl.activeTexture(gl.TEXTURE0)
    gl.bindTexture(gl.TEXTURE_2D, this.fbos.reflectionTexture)
    gl.activeTexture(gl.TEXTURE1)
    gl.bindTexture(gl.TEXTURE_2D, this.fbos.refractionTexture)
    gl.activeTexture(gl.TEXTURE2)
    gl.bindTexture(gl.TEXTURE_2D, this.dudvMap.id)
    gl.activeTexture(gl.TEXTURE3)
    gl.bindTexture(gl.TEXTURE_2D, this.normalMap.id)
    gl.activeTexture(gl.TEXTURE4)
    gl.bindTexture(gl.TEXTURE_2D, this.fbos.refractionDepthTexture)
  }

  draw (gl, waters, cam, sun) {
    this.shader.use(gl)

    gl.enable(gl.BLEND)
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)

    // Send camera matrices to the shader
    cam.draw(gl, this.shader)

    // Bind uniforms and textures
    this.prepare(gl, cam, sun)

    // Render a mesh at every tile location
    this.mesh.prepare(gl, this.shader)
    for (let i in waters) {
      const transformLoc = this.shader.getUniformLocation(gl, 'uModelMatrix')
      gl.uniformMatrix4fv(transformLoc, false, waters[i].transform)

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

    gl.disable(gl.BLEND)
  }

  update (dt) {
    this.phase += this.waveSpeed
    if (this.phase >= 1) this.phase = 0
  }
}

export { WaterRenderer, WaterTile }
