import Resource from './resource'

class Shader {
  constructor (type, source) {
    this.type = type
    this.source = source
    this.id = 0
  }

  compile (gl) {
    const shader = gl.createShader(this.type)

    // Send the source to the shader object
    gl.shaderSource(shader, this.source)

    // Compile the shader program
    gl.compileShader(shader)

    // See if it compiled successfully
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      let inf = gl.getShaderInfoLog(shader)
      gl.deleteShader(shader)
      throw new Error('An error occurred compiling the shaders: ' + inf)
    }

    this.id = shader

    return this
  }
}

class ShaderProgram {
  constructor (name) {
    this.name = name
    this.id = 0

    this.vertexShader = null
    this.geometryShader = null
    this.fragmentShader = null

    this.uniforms = {}
    this.attribs = {}
  }

  link (gl, vs, fs, gs) {
    let vsh = [ vs, fs, gs ]

    for (let i in vsh) {
      vsh[i] && vsh[i].compile(gl)
    }

    const shaderProgram = gl.createProgram()

    // Attach the shaders
    gl.attachShader(shaderProgram, vs.id)
    gs && gl.attachShader(shaderProgram, gs.id)
    gl.attachShader(shaderProgram, fs.id)

    // Link the program
    gl.linkProgram(shaderProgram)

    // If creating the shader program failed, error
    if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
      throw new Error('Unable to initialize the shader program ' + this.name + ': ' + gl.getProgramInfoLog(shaderProgram))
    }

    this.id = shaderProgram
    this.vertexShader = vs
    this.fragmentShader = fs
    this.geometryShader = gs

    // Detach shaders after use
    gl.detachShader(shaderProgram, vs.id)
    gs && gl.detachShader(shaderProgram, gs.id)
    gl.detachShader(shaderProgram, fs.id)

    return this
  }

  getUniformLocation (gl, name) {
    if (this.uniforms[name]) return this.uniforms[name]
    let uni = gl.getUniformLocation(this.id, name)
    if (uni < 0) return null

    this.uniforms[name] = uni
    return uni
  }

  getAttributeLocation (gl, name) {
    if (this.attribs[name]) return this.attribs[name]
    let pos = gl.getAttribLocation(this.id, name)
    if (pos < 0) throw new Error(`No such attribute location ${name} in shader ${this.name}!`)
    this.attribs[name] = pos
    return pos
  }

  hasAttribute (gl, name) {
    try {
      this.getAttributeLocation(gl, name)
      return true
    } catch (e) {
      return false
    }
  }

  setAttribute (gl, name, size, normalized, stride, offset, type) {
    let loc = this.getAttributeLocation(gl, name) // throws an error in case the name doesn't exist in shader

    gl.enableVertexAttribArray(loc)
    gl.vertexAttribPointer(
      loc,
      size,
      type || gl.FLOAT,
      normalized,
      stride,
      offset)
  }

  use (gl) {
    if (this.id === 0) return
    gl.useProgram(this.id)
  }
}

class ShaderManager {
  constructor () {
    this.shaders = {}
  }

  createShader (gl, name, vs, fs, gs) {
    if (this.shaders[name]) return this.shaders[name]
    let shader = new ShaderProgram(name)

    let vert = new Shader(gl.VERTEX_SHADER, vs)
    let frag = new Shader(gl.FRAGMENT_SHADER, fs)
    let geom = gs ? new Shader(gl.GEOMETRY_SHADER, gs) : null

    shader.link(gl, vert, frag, geom)

    this.shaders[name] = shader
    return shader
  }

  // Standard shader nomenclature: /assets/shaders/shader-name.vs|fs[|gs]
  // shader-name.vs and shader-name.fs are mandatory!
  createShaderFromFiles (gl, name, gs) {
    let stdloc = '/assets/shaders/' + name

    return new Promise((resolve, reject) => {
      function finishLink (vs, fs, gss) {
        try {
          let shader = this.createShader(gl, name, vs, fs, gss)
          resolve(shader)
        } catch (e) {
          reject(e)
        }
      }

      Resource.GET(stdloc + '.vs').then((vs) => {
        Resource.GET(stdloc + '.fs').then((fs) => {
          if (gs !== false) {
            // Try to find the geometry shader if it wasn't explicitly stated to not exist
            return Resource.GET(stdloc + '.gs').then((gss) => {
              finishLink.apply(this, [vs, fs, gss])
            }, () => {
              finishLink.apply(this, [vs, fs])
            })
          }

          finishLink.apply(this, [vs, fs, null])
        }, reject)
      }, reject)
    })
  }

  use (gl, name) {
    if (this.shaders[name]) this.shaders[name].use(gl)
  }
}

export { Shader, ShaderProgram, ShaderManager }
