import { useEffect, useRef } from "react";

import BLACKHOLE_SHADER_CODE from "./assets/blackhole.glsl?raw";
import STAR_SHADER_CODE from "./assets/star.glsl?raw";
import BALL_SHADER_CODE from "./assets/ball.glsl?raw";
import REFLECT_BALL_SHADER_CODE from "./assets/reflect.glsl?raw";
import CLOUDS_SHADER_CODE from "./assets/clouds.glsl?raw";

function buildProgram(
  ctx: WebGL2RenderingContext,
  fragShader: WebGLShader,
  vertexShader: WebGLShader
) {
  const program = ctx.createProgram();
  if (!program) return null;

  ctx.attachShader(program, fragShader);
  ctx.attachShader(program, vertexShader);

  ctx.linkProgram(program);

  const status = ctx.getProgramParameter(program, ctx.LINK_STATUS);

  if (status) {
    return program;
  }

  console.error(ctx.getProgramInfoLog(program));
  ctx.deleteProgram(program);
  return null;
}

function buildShader(
  ctx: WebGL2RenderingContext,
  type: number,
  source: string
) {
  const shader = ctx.createShader(type);
  if (!shader) return null;

  ctx.shaderSource(shader, source);
  ctx.compileShader(shader);

  const status = ctx.getShaderParameter(shader, ctx.COMPILE_STATUS);

  if (status) {
    return shader;
  }

  console.error(ctx.getShaderInfoLog(shader));
  ctx.deleteShader(shader);
  return null;
}

function loadTexture(gl: WebGL2RenderingContext, url: string) {
  const texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);

  // Because images have to be downloaded over the internet
  // they might take a moment until they are ready.
  // Until then put a single pixel in the texture so we can
  // use it immediately. When the image has finished downloading
  // we'll update the texture with the contents of the image.
  const level = 0;
  const internalFormat = gl.RGBA;
  const width = 1;
  const height = 1;
  const border = 0;
  const srcFormat = gl.RGBA;
  const srcType = gl.UNSIGNED_BYTE;
  const pixel = new Uint8Array([0, 0, 255, 255]); // opaque blue
  gl.texImage2D(
    gl.TEXTURE_2D,
    level,
    internalFormat,
    width,
    height,
    border,
    srcFormat,
    srcType,
    pixel
  );

  const image = new Image();
  image.onload = () => {
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(
      gl.TEXTURE_2D,
      level,
      internalFormat,
      srcFormat,
      srcType,
      image
    );

    // WebGL1 has different requirements for power of 2 images
    // vs. non power of 2 images so check if the image is a
    // power of 2 in both dimensions.
    if (isPowerOf2(image.width) && isPowerOf2(image.height)) {
      // Yes, it's a power of 2. Generate mips.
      gl.generateMipmap(gl.TEXTURE_2D);
    } else {
      // No, it's not a power of 2. Turn off mips and set
      // wrapping to clamp to edge
      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.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    }
  };
  image.src = url;

  return texture;
}

function isPowerOf2(value: number) {
  return (value & (value - 1)) === 0;
}

type ShaderBackgroundProps = {
  theme: string;
};

export const ShaderBackground: React.FC<ShaderBackgroundProps> = ({
  theme,
}) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const gl = canvas.getContext("webgl2");
    if (!gl) return;

    gl.viewport(0, 0, canvas.width, canvas.height);
    gl.clearColor(0, 0, 0, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);

    let shader_code;
    switch (theme) {
      case "blackhole":
        shader_code = BLACKHOLE_SHADER_CODE;
        break;
      case "star":
        shader_code = STAR_SHADER_CODE;
        break;
      case "ball":
        shader_code = BALL_SHADER_CODE;
        break;
      case "reflect":
        shader_code = REFLECT_BALL_SHADER_CODE;
        break;
      case "clouds":
        shader_code = CLOUDS_SHADER_CODE;
        break;
      default:
        return;
    }

    const shader = buildShader(gl, gl.FRAGMENT_SHADER, shader_code);
    const vertexShader = buildShader(
      gl,
      gl.VERTEX_SHADER,
      `#version 300 es
    precision highp float;
    
    in vec2 a_position;
    
    void main() {
        gl_Position = vec4(a_position, 0.0, 1.0);
    }`
    );

    if (!shader || !vertexShader) return;

    const finalProgram = buildProgram(gl, shader, vertexShader);

    if (!finalProgram) return;

    const posBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
    gl.bufferData(
      gl.ARRAY_BUFFER,
      new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
      gl.STATIC_DRAW
    );

    gl.useProgram(finalProgram);
    const loc = gl.getAttribLocation(finalProgram, "a_position");
    gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(loc);

    function setResolution(program: WebGLProgram) {
      const loc = gl!.getUniformLocation(program, "iResolution");
      canvas!.width = window.visualViewport!.width;
      canvas!.height = window.visualViewport!.height;
      if (loc) gl!.uniform3fv(loc, [
        window.visualViewport!.width,
        window.visualViewport!.height,
        1,
      ]);
    }

    function setTime(program: WebGLProgram, now: number) {
      const loc = gl!.getUniformLocation(program, "iTime");
      if (loc) gl!.uniform1f(loc, now);
    }

    const final_iChannel1 = gl.getUniformLocation(finalProgram, "iChannel1");

    const ichannel1_texture = loadTexture(gl, "assets/small_noise.png");

    gl!.viewport(
      0,
      0,
      window.visualViewport!.width,
      window.visualViewport!.height
    );

    gl!.useProgram(finalProgram);
    setResolution(finalProgram!);

    // Bind Texture to Unit 1
    gl!.activeTexture(gl!.TEXTURE1);
    gl!.bindTexture(gl!.TEXTURE_2D, ichannel1_texture);
    gl!.uniform1i(final_iChannel1, 1);

    let last_update: number = 0;

    const compiled_theme = theme;

    function update(now: number) {
      // time in seconds
      const time = now / 1000;

      if (compiled_theme !== theme) {
        return;
      }

      if (time - last_update < (1 / 30)) {
        requestAnimationFrame(update);
        return;
      };
      last_update = time;

      gl!.clear(gl!.COLOR_BUFFER_BIT);

      setTime(finalProgram!, time);

      gl!.drawArrays(gl!.TRIANGLES, 0, 6);

      requestAnimationFrame(update);
    }

    requestAnimationFrame(update);
  }, [theme]);

  return (
    <canvas
      ref={canvasRef}
      id="blackhole_canvas"
      style={{
        width: "100vw",
        height: "100vh",
        position: "fixed",
        top: 0,
        left: 0,
        zIndex: -1,
      }}
    />
  );
};
