import { onMount, onCleanup, splitProps } from "solid-js";

const VERTEX_SHADER = `
precision highp float;

float mod289(float x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec2 mod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }

float permute(float x) { return mod289(((x * 34.0) + 1.0) * x); }
vec2 permute(vec2 x) { return mod289(((x * 34.0) + 1.0) * x); }
vec3 permute(vec3 x) { return mod289(((x * 34.0) + 1.0) * x); }
vec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }

float psrdnoise(vec2 x, vec2 period, float alpha, out vec2 gradient) {
  vec2 uv = vec2(x.x + x.y * 0.5, x.y);
  vec2 i0 = floor(uv);
  vec2 f0 = fract(uv);
  float cmp = step(f0.y, f0.x);
  vec2 o1 = vec2(cmp, 1.0 - cmp);
  vec2 i1 = i0 + o1;
  vec2 i2 = i0 + vec2(1.0, 1.0);
  vec2 v0 = vec2(i0.x - i0.y * 0.5, i0.y);
  vec2 v1 = vec2(v0.x + o1.x - o1.y * 0.5, v0.y + o1.y);
  vec2 v2 = vec2(v0.x + 0.5, v0.y + 1.0);
  vec2 x0 = x - v0;
  vec2 x1 = x - v1;
  vec2 x2 = x - v2;
  vec3 iu, iv;
  vec3 xw, yw;
  if(any(greaterThan(period, vec2(0.0)))) {
    xw = vec3(v0.x, v1.x, v2.x);
    yw = vec3(v0.y, v1.y, v2.y);
    if(period.x > 0.0) xw = mod(vec3(v0.x, v1.x, v2.x), period.x);
    if(period.y > 0.0) yw = mod(vec3(v0.y, v1.y, v2.y), period.y);
    iu = floor(xw + 0.5 * yw + 0.5);
    iv = floor(yw + 0.5);
  } else {
    iu = vec3(i0.x, i1.x, i2.x);
    iv = vec3(i0.y, i1.y, i2.y);
  }
  vec3 hash = mod(iu, 289.0);
  hash = mod((hash * 51.0 + 2.0) * hash + iv, 289.0);
  hash = mod((hash * 34.0 + 10.0) * hash, 289.0);
  vec3 psi = hash * 0.07482 + alpha;
  vec3 gx = cos(psi);
  vec3 gy = sin(psi);
  vec2 g0 = vec2(gx.x, gy.x);
  vec2 g1 = vec2(gx.y, gy.y);
  vec2 g2 = vec2(gx.z, gy.z);
  vec3 w = 0.8 - vec3(dot(x0, x0), dot(x1, x1), dot(x2, x2));
  w = max(w, 0.0);
  vec3 w2 = w * w;
  vec3 w4 = w2 * w2;
  vec3 gdotx = vec3(dot(g0, x0), dot(g1, x1), dot(g2, x2));
  float n = dot(w4, gdotx);
  vec3 w3 = w2 * w;
  vec3 dw = -8.0 * w3 * gdotx;
  vec2 dn0 = w4.x * g0 + dw.x * x0;
  vec2 dn1 = w4.y * g1 + dw.y * x1;
  vec2 dn2 = w4.z * g2 + dw.z * x2;
  gradient = 10.9 * (dn0 + dn1 + dn2);
  return 10.9 * n;
}

float psrdnoise(vec3 x, vec3 period, float alpha, out vec3 gradient) {
  const mat3 M = mat3(0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0);
  const mat3 Mi = mat3(-0.5, 0.5, 0.5, 0.5, -0.5, 0.5, 0.5, 0.5, -0.5);
  vec3 uvw = M * x;
  vec3 i0 = floor(uvw);
  vec3 f0 = fract(uvw);
  vec3 g_ = step(f0.xyx, f0.yzz);
  vec3 l_ = 1.0 - g_;
  vec3 g = vec3(l_.z, g_.xy);
  vec3 l = vec3(l_.xy, g_.z);
  vec3 o1 = min(g, l);
  vec3 o2 = max(g, l);
  vec3 i1 = i0 + o1;
  vec3 i2 = i0 + o2;
  vec3 i3 = i0 + vec3(1.0);
  vec3 v0 = Mi * i0;
  vec3 v1 = Mi * i1;
  vec3 v2 = Mi * i2;
  vec3 v3 = Mi * i3;
  vec3 x0 = x - v0;
  vec3 x1 = x - v1;
  vec3 x2 = x - v2;
  vec3 x3 = x - v3;
  if(any(greaterThan(period, vec3(0.0)))) {
    vec4 vx = vec4(v0.x, v1.x, v2.x, v3.x);
    vec4 vy = vec4(v0.y, v1.y, v2.y, v3.y);
    vec4 vz = vec4(v0.z, v1.z, v2.z, v3.z);
    if(period.x > 0.0) vx = mod(vx, period.x);
    if(period.y > 0.0) vy = mod(vy, period.y);
    if(period.z > 0.0) vz = mod(vz, period.z);
    i0 = M * vec3(vx.x, vy.x, vz.x);
    i1 = M * vec3(vx.y, vy.y, vz.y);
    i2 = M * vec3(vx.z, vy.z, vz.z);
    i3 = M * vec3(vx.w, vy.w, vz.w);
    i0 = floor(i0 + 0.5);
    i1 = floor(i1 + 0.5);
    i2 = floor(i2 + 0.5);
    i3 = floor(i3 + 0.5);
  }
  vec4 hash = permute(permute(permute(vec4(i0.z, i1.z, i2.z, i3.z)) + vec4(i0.y, i1.y, i2.y, i3.y)) + vec4(i0.x, i1.x, i2.x, i3.x));
  vec4 theta = hash * 3.883222077;
  vec4 sz = hash * -0.006920415 + 0.996539792;
  vec4 psi = hash * 0.108705628;
  vec4 Ct = cos(theta);
  vec4 St = sin(theta);
  vec4 sz_prime = sqrt(1.0 - sz * sz);
  vec4 gx, gy, gz;
  if(alpha != 0.0) {
    vec4 Sp = sin(psi);
    vec4 Cp = cos(psi);
    vec4 px = Ct * sz_prime;
    vec4 py = St * sz_prime;
    vec4 pz = sz;
    vec4 Ctp = St * Sp - Ct * Cp;
    vec4 qx = mix(Ctp * St, Sp, sz);
    vec4 qy = mix(-Ctp * Ct, Cp, sz);
    vec4 qz = -(py * Cp + px * Sp);
    vec4 Sa = vec4(sin(alpha));
    vec4 Ca = vec4(cos(alpha));
    gx = Ca * px + Sa * qx;
    gy = Ca * py + Sa * qy;
    gz = Ca * pz + Sa * qz;
  } else {
    gx = Ct * sz_prime;
    gy = St * sz_prime;
    gz = sz;
  }
  vec3 g0 = vec3(gx.x, gy.x, gz.x);
  vec3 g1 = vec3(gx.y, gy.y, gz.y);
  vec3 g2 = vec3(gx.z, gy.z, gz.z);
  vec3 g3 = vec3(gx.w, gy.w, gz.w);
  vec4 w = 0.5 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3));
  w = max(w, 0.0);
  vec4 w2 = w * w;
  vec4 w3 = w2 * w;
  vec4 gdotx = vec4(dot(g0, x0), dot(g1, x1), dot(g2, x2), dot(g3, x3));
  float n = dot(w3, gdotx);
  vec4 dw = -6.0 * w2 * gdotx;
  vec3 dn0 = w3.x * g0 + dw.x * x0;
  vec3 dn1 = w3.y * g1 + dw.y * x1;
  vec3 dn2 = w3.z * g2 + dw.z * x2;
  vec3 dn3 = w3.w * g3 + dw.w * x3;
  gradient = 39.5 * (dn0 + dn1 + dn2 + dn3);
  return 39.5 * n;
}

attribute vec3 tangent;

uniform vec3 u_wave;
uniform float u_scale;

varying vec2 v_texcoord;
varying vec4 v_noise;
varying vec3 v_worldPosition;
varying vec3 v_viewNormal;

float getNoise(vec3 pos, float time, out vec3 gradient) {
  return psrdnoise(pos, vec3(0.0), time, gradient);
}

vec3 displace(vec3 pos, float time) {
  float nearCut = step(pos.x, 0.2499);
  float waveAmpMul = 1.2 + u_wave.y;
  float scaleAmt = u_scale * 0.36;

  vec3 gradient = vec3(0.0);

  vec3 posOffset1 = vec3(0.0, time * 0.12, 0.0);
  float t1 = time * 0.24 + 11.111;
  float n1 = getNoise(pos * 2.2 + posOffset1, t1, gradient);
  pos.z += n1 * scaleAmt * waveAmpMul;

  float n2 = getNoise(pos * vec3(42.0, 0.0, 0.0), 0.0, gradient);
  pos.z += n2 * 0.016 * nearCut;
  v_noise.y = n2;

  return pos;
}

void main() {
  vec3 pos = position;
  vec3 calcNormal = normal;
  v_noise = vec4(0.0);

  float waveTime = u_wave.x;
  pos = displace(pos, waveTime);

  vec3 gradient = vec3(0.0);
  float n3 = getNoise(position * vec3(2.4, 1.2, 1.0) + vec3(0.0, waveTime * -0.06, 0.0), waveTime * 0.4 + 6.666, gradient);
  v_noise.x = n3;

  float isEnd = 1.0 - step(pos.x, 0.2499);
  pos.z += isEnd * 0.01;
  v_noise.z = isEnd;

  float offset = 0.03;
  vec3 biTangent = cross(calcNormal, tangent.xyz);
  calcNormal = cross(displace(position.xyz - tangent.xyz * offset, waveTime) - pos, displace(position.xyz - biTangent.xyz * offset, waveTime) - pos);

  vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
  gl_Position = projectionMatrix * mvPosition;

  v_texcoord = uv;
  v_worldPosition = (modelMatrix * vec4(pos, 1.0)).xyz;
  v_viewNormal = normalMatrix * calcNormal;
}
`;

const FRAGMENT_SHADER = `
precision highp float;

uniform vec4 u_light;

varying vec2 v_texcoord;
varying vec4 v_noise;
varying vec3 v_worldPosition;
varying vec3 v_viewNormal;

float map(float value, float min1, float max1, float min2, float max2) {
  return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
}

vec3 inverseTransformDirection(in vec3 dir, in mat4 matrix) {
  return normalize((vec4(dir, 0.0) * matrix).xyz);
}

vec3 paletteBrand(float t) {
  vec3 deep = vec3(0.263, 0.220, 0.792);
  vec3 mid = vec3(0.310, 0.275, 0.898);
  vec3 bright = vec3(0.506, 0.549, 0.973);
  vec3 accent = vec3(0.024, 0.714, 0.831);
  t = clamp(t, 0.0, 1.0);
  if (t < 0.33) return mix(deep, mid, t / 0.33);
  if (t < 0.66) return mix(mid, bright, (t - 0.33) / 0.33);
  return mix(bright, accent, (t - 0.66) / 0.34);
}

void main() {
  float n = v_noise.x;
  float nFine = v_noise.y;
  float uv_y = v_texcoord.y;
  float uv_x = v_texcoord.x;

  float noiseRemap = n * 0.5 + 0.5;
  float fineRemap = nFine * 0.5 + 0.5;
  float edgeFade = smoothstep(0.0, 0.15, uv_y) * smoothstep(1.0, 0.85, uv_y);
  edgeFade *= smoothstep(0.0, 0.1, uv_x) * smoothstep(1.0, 0.9, uv_x);

  float colorT = noiseRemap * 0.6 + fineRemap * 0.15 + uv_y * 0.25;
  colorT += sin(uv_x * 3.14159 + n * 1.2) * 0.08;
  colorT = clamp(colorT, 0.0, 1.0);

  vec3 color = paletteBrand(colorT);
  color *= mix(0.55, 1.0, edgeFade);

  vec3 viewDir = normalize(cameraPosition - v_worldPosition);
  vec3 viewNormal = normalize(v_viewNormal);
  vec3 worldNormal = inverseTransformDirection(viewNormal, viewMatrix);
  vec3 lightDir = u_light.xyz;

  float dirLight = max(dot(lightDir, worldNormal), 0.0);

  vec3 halfwayVector = normalize(lightDir + viewDir);
  float shininess = 6.0;
  float specular = pow(max(dot(halfwayVector, worldNormal), 0.0), shininess);
  specular = smoothstep(0.0, 1.5, specular);
  vec3 specularColor = mix(vec3(0.5, 0.5, 1.0), vec3(1.0), 0.5) * specular;

  float highlights = smoothstep(0.15, 0.85, fineRemap) * 0.2;
  dirLight = map(dirLight, 0.0, 1.0, 0.5, 0.78);

  vec3 lightedColor = dirLight * color;
  lightedColor += specularColor;
  lightedColor += highlights * vec3(0.024, 0.714, 0.831);

  float fresnel = 1.0 - pow(abs(dot(viewNormal, viewDir)), 0.25);
  lightedColor += fresnel * vec3(0.15, 0.12, 0.40);

  lightedColor = mix(lightedColor, vec3(1.0), 0.08);

  float edgeSoftness = 1.0 - fresnel * 0.3;
  edgeSoftness *= mix(0.85, 1.0, edgeFade);
  gl_FragColor = vec4(lightedColor, edgeSoftness);
}
`;

const STARTING_WAVE_HEIGHT = -0.6;

function easeOutQuart(t: number) {
  return 1 - Math.pow(1 - t, 4);
}

type ColorWaveBackgroundProps = {
  yOffset?: number;
  scale?: number;
  speed?: number;
};

export function ColorWaveBackground(props: ColorWaveBackgroundProps) {
  let containerRef: HTMLDivElement | undefined;
  const yOffset = props.yOffset ?? 0.25;
  const scale = props.scale ?? 1;
  const speed = props.speed ?? 0.55;

  onMount(async () => {
    if (!containerRef) return;

    const THREE = await import("three");

    const prefersReducedMotion = window.matchMedia(
      "(prefers-reduced-motion: reduce)",
    ).matches;

    const renderer = new THREE.WebGLRenderer({
      alpha: true,
      antialias: true,
      powerPreference: "high-performance",
    });
    renderer.setClearColor(0xffffff, 0);
    renderer.setPixelRatio(Math.min(2, window.devicePixelRatio));
    renderer.setSize(containerRef.clientWidth, containerRef.clientHeight);

    const canvas = renderer.domElement;
    canvas.style.position = "absolute";
    canvas.style.top = "0";
    canvas.style.left = "0";
    canvas.style.width = "100%";
    canvas.style.height = "100%";
    containerRef.appendChild(canvas);

    const scene = new THREE.Scene();

    const camera = new THREE.PerspectiveCamera(
      50,
      containerRef.clientWidth / containerRef.clientHeight,
      0.1,
      100,
    );
    camera.position.z = 1;
    camera.updateProjectionMatrix();

    const isMobile = window.innerWidth <= 600;
    const segmentMultiplier = isMobile ? 0.7 : 1;
    const geometry = new THREE.PlaneGeometry(
      0.5,
      2,
      Math.floor(132 * segmentMultiplier),
      Math.floor(200 * segmentMultiplier),
    );
    geometry.computeVertexNormals();

    const tangentCount = geometry.attributes.position.count;
    const tangentArray = new Float32Array(tangentCount * 4);
    for (let i = 0; i < tangentCount; i++) {
      tangentArray[i * 4] = 1;
      tangentArray[i * 4 + 1] = 0;
      tangentArray[i * 4 + 2] = 0;
      tangentArray[i * 4 + 3] = 1;
    }
    geometry.setAttribute(
      "tangent",
      new THREE.BufferAttribute(tangentArray, 4),
    );

    const uniforms = {
      u_wave: { value: new THREE.Vector3(0, STARTING_WAVE_HEIGHT, 0) },
      u_light: { value: new THREE.Vector4(-1, -1, 1, 1) },
      u_scale: { value: scale },
    };

    const material = new THREE.ShaderMaterial({
      vertexShader: VERTEX_SHADER,
      fragmentShader: FRAGMENT_SHADER,
      uniforms,
      side: THREE.DoubleSide,
    });

    const mesh = new THREE.Mesh(geometry, material);

    const isMobileAtMount = window.innerWidth <= 600;
    const baseYOffset = isMobileAtMount ? yOffset - 0.08 : yOffset;

    function setPlaneTransform() {
      mesh.rotation.set(
        Math.PI / 2,
        Math.PI / -0.5 + Math.PI * 0.02,
        Math.PI / 2 + Math.PI * 0.09,
      );
      mesh.scale.set(2, 2.5, 1);
      mesh.position.y = baseYOffset;
      if (isMobileAtMount) {
        mesh.scale.multiplyScalar(0.8);
      }
    }
    setPlaneTransform();
    scene.add(mesh);

    let animationId = 0;
    const timer = new THREE.Timer();
    let introStartTime = 0;
    let introComplete = false;
    let canvasOpacity = 0;

    function animate(timestamp = 0) {
      timer.update();
      const delta = timer.getDelta();
      const elapsed = timer.getElapsed();

      uniforms.u_wave.value.x += delta * speed;
      uniforms.u_light.value.x = Math.sin(elapsed * 0.5);
      uniforms.u_light.value.y = -1 + 0.2 * Math.sin(elapsed);

      if (!introComplete) {
        if (introStartTime === 0) introStartTime = timestamp;
        const progress = Math.min((timestamp - introStartTime) / 3000, 1);
        const eased = easeOutQuart(progress);
        uniforms.u_wave.value.y =
          STARTING_WAVE_HEIGHT + (0 - STARTING_WAVE_HEIGHT) * eased;

        if (progress < 1) {
          canvasOpacity = Math.min(canvasOpacity + delta / 2, 1);
          canvas.style.opacity = String(canvasOpacity);
        } else {
          introComplete = true;
          canvas.style.opacity = "1";
        }
      }

      if (!prefersReducedMotion) {
        renderer.render(scene, camera);
      }

      animationId = requestAnimationFrame(animate);
    }

    if (prefersReducedMotion) {
      uniforms.u_wave.value.y = 0;
      canvas.style.opacity = "1";
      renderer.render(scene, camera);
    } else {
      canvas.style.opacity = "0";
      animationId = requestAnimationFrame(animate);
    }

    let lastWidth = containerRef.clientWidth;

    function onResize() {
      const w = containerRef?.clientWidth ?? lastWidth;
      if (w === lastWidth) return;
      lastWidth = w;
      const h = containerRef?.clientHeight ?? 1080;
      renderer.setSize(w, h);
      camera.aspect = w / h;
      camera.updateProjectionMatrix();
      setPlaneTransform();
    }

    function onVisibilityChange() {
      uniforms.u_wave.value.x = 0;
    }

    window.addEventListener("resize", onResize);
    document.addEventListener("visibilitychange", onVisibilityChange);

    onCleanup(() => {
      cancelAnimationFrame(animationId);
      window.removeEventListener("resize", onResize);
      document.removeEventListener("visibilitychange", onVisibilityChange);
      geometry.dispose();
      material.dispose();
      renderer.dispose();
    });
  });

  return (
    <div
      ref={containerRef}
      class="fixed top-0 left-0 pointer-events-none"
      style={{ width: "100lvw", height: "100lvh", "z-index": 0 }}
    />
  );
}
