const DEFAULT_VERTEX_SHADER = `
  attribute float aVertexIndex;
  varying highp vec2 vTextureCoord;

 	// This vertex shader produces the following, when drawn using indices 0..3:
	//
	//  1 |  0-----x.....2
	//  0 |  |  s  |  . ´
	// -1 |  x_____x´
	// -2 |  :  .´
	// -3 |  1´
	//    +---------------
	//      -1  0  1  2  3
	//
	// The axes are clip-space x and y. The region marked s is the visible region.
	// The digits in the corners of the right-angled triangle are the vertex
	// indices.
	//
	// The top-left has UV 0,0, the bottom-left has 0,2, and the top-right has 2,0.
	// This means that the UV gets interpolated to 1,1 at the bottom-right corner
	// of the clip-space rectangle that is at 1,-1 in clip space.

	void main() {
	    vec2 uv = vec2(floor(aVertexIndex / 2.0), floor(mod(aVertexIndex, 2.0))) * 2.0;
	    gl_Position = vec4(uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0), 0.0, 1.0);
      vTextureCoord = uv;
	}
`

const FRAGMENT_SHADER_UTILITIES = `
  highp float noise1(highp vec2 co){
      return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453);
  }

  // Color spaces from https://github.com/tobspr/GLSL-Color-Spaces/blob/master/ColorSpaces.inc.glsl

  const highp float HCV_EPSILON = 1e-10;
  const highp float HSL_EPSILON = 1e-10;

  highp vec3 hue_to_rgb(highp float hue)
  {
      highp float R = abs(hue * 6.0 - 3.0) - 1.0;
      highp float G = 2.0 - abs(hue * 6.0 - 2.0);
      highp float B = 2.0 - abs(hue * 6.0 - 4.0);
      return clamp(vec3(R,G,B), vec3(0), vec3(1));
  }

  // Converts from HSL to linear RGB
  highp vec3 hsl_to_rgb(highp vec3 hsl)
  {
      highp vec3 rgb = hue_to_rgb(hsl.x);
      highp float C = (1.0 - abs(2.0 * hsl.z - 1.0)) * hsl.y;
      return (rgb - 0.5) * C + hsl.z;
  }

  // Converts a value from linear RGB to HCV (Hue, Chroma, Value)
  highp vec3 rgb_to_hcv(highp vec3 rgb)
  {
      // Based on work by Sam Hocevar and Emil Persson
      highp vec4 P = (rgb.g < rgb.b) ? vec4(rgb.bg, -1.0, 2.0/3.0) : vec4(rgb.gb, 0.0, -1.0/3.0);
      highp vec4 Q = (rgb.r < P.x) ? vec4(P.xyw, rgb.r) : vec4(rgb.r, P.yzx);
      highp float C = Q.x - min(Q.w, Q.y);
      highp float H = abs((Q.w - Q.y) / (6.0 * C + HCV_EPSILON) + Q.z);
      return vec3(H, C, Q.x);
  }

  // Converts from linear rgb to HSL
  highp vec3 rgb_to_hsl(highp vec3 rgb)
  {
      highp vec3 HCV = rgb_to_hcv(rgb);
      highp float L = HCV.z - HCV.y * 0.5;
      highp float S = HCV.y / (1.0 - abs(L * 2.0 - 1.0) + HSL_EPSILON);
      return vec3(HCV.x, S, L);
  }

  highp vec3 hue_rotate(highp vec3 rgb_input, highp float amount) {
    highp vec3 hsl = rgb_to_hsl(rgb_input);
    hsl.x = mod(hsl.x+amount,1.0);
    return hsl_to_rgb(hsl);
  }
`

const FRAGMENT_SHADER_UTILITIES_N_LINES = FRAGMENT_SHADER_UTILITIES.split(/\n/g).length

const DEFAULT_FRAGMENT_SHADER = `
  // Some random values computed for each frame
  uniform highp vec4 uRandom;
  // Size (in logical pixel) of current CuteTexture canvas
  uniform lowp vec2 uCanvasSize;
  // Number of seconds since canvas was initialized
  uniform lowp float uTime;
  // Coordinates of current pixel being computed from [0,0] (top left) to [1,1] (bottom right)
  varying highp vec2 vTextureCoord;

  void main() {
    gl_FragColor = vec4(vTextureCoord.x, vTextureCoord.y, 0.0, 1.0);
  }
`

/**
 * Initialise un canvas d'arrière plan "CuteTexture" avec les options données
 */
export function initCuteTexture(){
  /** @type {HTMLCanvasElement} */
  const targetCanvas = document.querySelector("canvas[data-cute-texture]") || document.querySelector("canvas.cute-texture") || document.querySelector("canvas");
  if(!targetCanvas)
    throw new Error(`Aucun élément <canvas> trouvé sur la page`)

  {
    let computedStyle = getComputedStyle(targetCanvas)
    let elementWidth = parseFloat(computedStyle.width)
    let elementHeight = parseFloat(computedStyle.height)
    targetCanvas.width = elementWidth
    targetCanvas.height = elementHeight
  }

  const ctx = targetCanvas.getContext("webgl");
  if(ctx == null)
    throw new Error("WebGL n'ext pas supporté sur votre machine")

  let mainShader = initMainShader(ctx)
  console.log("Shader CuteTexture initialisé", mainShader)
  
  const initTime = Date.now()

  function startRender(){
    renderCuteTexture(targetCanvas, mainShader, initTime)
    requestAnimationFrame(startRender)
  }

  startRender()

  return {
    target: targetCanvas,
    initTime,
    resize: function(width, height) {
      targetCanvas.width = width
      targetCanvas.height = height
    }
  }
}

/**
 * @param {WebGLRenderingContext} ctx 
 */
function initMainShader(ctx, oldShader=null){
  const vertexShader = ctx.createShader(ctx.VERTEX_SHADER)
  ctx.shaderSource(vertexShader, DEFAULT_VERTEX_SHADER)
  ctx.compileShader(vertexShader)

  if (!ctx.getShaderParameter(vertexShader, ctx.COMPILE_STATUS)) {
    throw new Error(`Le vertex shader n'a pas pu être compilé: ${ctx.getShaderInfoLog(vertexShader)}`)
    ctx.deleteShader(vertexShader)
    return null
  }

  let fragmentShaderCode;
  let fragmentShaderCodeElement = document.querySelector(`script[type="x-shader/glsl+fragment"]`);
  if(fragmentShaderCodeElement){
    fragmentShaderCode = fragmentShaderCodeElement.innerHTML
  } else {
    fragmentShaderCode = DEFAULT_FRAGMENT_SHADER
  }

  const fragmentShader = ctx.createShader(ctx.FRAGMENT_SHADER)
  ctx.shaderSource(fragmentShader, FRAGMENT_SHADER_UTILITIES+fragmentShaderCode)
  ctx.compileShader(fragmentShader)

  if (!ctx.getShaderParameter(fragmentShader, ctx.COMPILE_STATUS)) {
    let error = ctx.getShaderInfoLog(fragmentShader)
    
    let errorLine = error.match(/\d+:(\d+):/)
    if(errorLine) {
      let lineNum = parseInt(errorLine[1])
      let lineNumeWithoutUtils = lineNum-FRAGMENT_SHADER_UTILITIES_N_LINES;
      if(lineNumeWithoutUtils >= 0){
        error = `Ligne ${lineNumeWithoutUtils}: ${error}`
      }
    }

    ctx.deleteShader(fragmentShader)
    throw new Error(`Le fragment shader n'a pas pu être compilé: ${error}`)
  }

  const shaderProgram = ctx.createProgram()
  ctx.attachShader(shaderProgram, vertexShader)
  ctx.attachShader(shaderProgram, fragmentShader)
  ctx.linkProgram(shaderProgram)

  if (!ctx.getProgramParameter(shaderProgram, ctx.LINK_STATUS)) {
    throw new Error(`Le shader n'a pas pu être linké: ${ctx.getProgramInfoLog(shaderProgram)}`)
    ctx.deleteShader(vertexShader)
    ctx.deleteShader(fragmentShader)
    ctx.deleteProgram(shaderProgram)
    return null
  }

  if(oldShader){
    ctx.deleteProgram(oldShader.program)
    ctx.deleteShader(oldShader.vertexShader)
    ctx.deleteShader(oldShader.fragmentShader)
    ctx.deleteBuffer(oldShader.vertexIndexBuffer)
  }

  const vertexIndexBuffer = ctx.createBuffer()
  ctx.bindBuffer(ctx.ARRAY_BUFFER, vertexIndexBuffer)
  ctx.bufferData(ctx.ARRAY_BUFFER, new Float32Array([0, 1, 2]), ctx.STATIC_DRAW)

  return {
    program: shaderProgram,
    vertexShader,
    fragmentShader,
    fragmentShaderElement: fragmentShaderCodeElement,
    aVertexIndex: ctx.getAttribLocation(shaderProgram, "aVertexIndex"),
    uRandom: ctx.getUniformLocation(shaderProgram, "uRandom"),
    uCanvasSize: ctx.getUniformLocation(shaderProgram, "uCanvasSize"),
    uTime: ctx.getUniformLocation(shaderProgram, "uTime"),
    vertexIndexBuffer,
  }
}

/**
 * @param {HTMLCanvasElement} target 
 * @param {*} mainShader 
 */
function renderCuteTexture(target, mainShader, initTime){
  let ctx = target.getContext("webgl")

  ctx.clearColor(0, 0, 0, 0)
  ctx.clearDepth(1.0)
  ctx.enable(ctx.DEPTH_TEST)
  ctx.depthFunc(ctx.LEQUAL)

  ctx.clear(ctx.COLOR_BUFFER_BIT)

  ctx.bindBuffer(ctx.ARRAY_BUFFER, mainShader.vertexIndexBuffer)
  ctx.vertexAttribPointer(
    mainShader.aVertexIndex, // location
    1, // pull out 1 value per iteration
    ctx.FLOAT, // the data in the buffer is 32bit floats
    false, // don't normalize
    0, // how many bytes to get from one set of values to the next
    0 // how many bytes inside the buffer to start from
  )
  ctx.enableVertexAttribArray(mainShader.aVertexIndex)

  ctx.useProgram(mainShader.program)
  if(mainShader.uRandom){
    ctx.uniform4f(
      mainShader.uRandom,
      Math.random(),
      Math.random(),
      Math.random(),
      Math.random()
    )
  }

  if(mainShader.uCanvasSize){
    ctx.uniform2f(
      mainShader.uCanvasSize,
      target.width,
      target.height
    )
  }

  if(mainShader.uTime){
    ctx.uniform1f(
      mainShader.uTime,
      (Date.now() - initTime)/1000
    )
  }
  
  ctx.drawArrays(ctx.TRIANGLE_STRIP, 0, 3);
}