/* global React, THREE */
/*
  ASCIIText — adapted from React Bits (reactbits.dev) for Lucy Liu's no-build site.
  Changes from upstream:
   - No ES module imports. Uses global THREE (CDN) and global React.
   - Mono font switched to Space Mono (the design system's mono voice), not IBM Plex Mono.
   - Hue-rotation LOCKED OFF: glyphs stay a single brand color (Signal/Atmospheric Blue)
     instead of cycling the whole spectrum. Honors the "one accent" rule.
   - Honors prefers-reduced-motion AND `.reduce-motion`: waves disabled + a single static
     frame is rendered (no animation loop, no mouse tilt).
   - Exposed as window.AsciiText.
*/

const AT_vertexShader = `
varying vec2 vUv;
uniform float uTime;
uniform float mouse;
uniform float uEnableWaves;
void main() {
    vUv = uv;
    float time = uTime * 5.;
    float waveFactor = uEnableWaves;
    vec3 transformed = position;
    transformed.x += sin(time + position.y) * 0.5 * waveFactor;
    transformed.y += cos(time + position.z) * 0.15 * waveFactor;
    transformed.z += sin(time + position.x) * waveFactor;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
}
`;

const AT_fragmentShader = `
varying vec2 vUv;
uniform float mouse;
uniform float uTime;
uniform sampler2D uTexture;
void main() {
    float time = uTime;
    vec2 pos = vUv;
    float r = texture2D(uTexture, pos + cos(time * 2. - time + pos.x) * .01).r;
    float g = texture2D(uTexture, pos + tan(time * .5 + pos.x - time) * .01).g;
    float b = texture2D(uTexture, pos - cos(time * 2. + time + pos.y) * .01).b;
    float a = texture2D(uTexture, pos).a;
    gl_FragColor = vec4(r, g, b, a);
}
`;

if (typeof Math.map !== 'function') {
  Math.map = function (n, start, stop, start2, stop2) {
    return ((n - start) / (stop - start)) * (stop2 - start2) + start2;
  };
}

const AT_PX_RATIO = typeof window !== 'undefined' ? window.devicePixelRatio : 1;

function atReducedMotion() {
  try {
    return (
      document.documentElement.classList.contains('reduce-motion') ||
      window.matchMedia('(prefers-reduced-motion: reduce)').matches
    );
  } catch (e) {
    return false;
  }
}

class AT_AsciiFilter {
  constructor(renderer, opts) {
    opts = opts || {};
    this.renderer = renderer;
    this.glyphColor = opts.glyphColor || '#A0C8FF';
    this.domElement = document.createElement('div');
    this.domElement.style.position = 'absolute';
    this.domElement.style.top = '0';
    this.domElement.style.left = '0';
    this.domElement.style.width = '100%';
    this.domElement.style.height = '100%';

    this.pre = document.createElement('pre');
    this.domElement.appendChild(this.pre);

    this.canvas = document.createElement('canvas');
    this.context = this.canvas.getContext('2d');
    this.domElement.appendChild(this.canvas);

    this.invert = opts.invert != null ? opts.invert : true;
    this.fontSize = opts.fontSize || 12;
    this.fontFamily = opts.fontFamily || "'Space Mono', 'Courier New', monospace";
    this.charset = opts.charset ||
      ' .\'`^",:;Il!i~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$';

    this.context.imageSmoothingEnabled = false;
  }

  setSize(width, height) {
    this.width = width;
    this.height = height;
    this.renderer.setSize(width, height);
    this.reset();
    this.center = { x: width / 2, y: height / 2 };
    this.mouse = { x: this.center.x, y: this.center.y };
  }

  reset() {
    this.context.font = this.fontSize + 'px ' + this.fontFamily;
    const charWidth = this.context.measureText('A').width;
    this.cols = Math.floor(this.width / (this.fontSize * (charWidth / this.fontSize)));
    this.rows = Math.floor(this.height / this.fontSize);
    this.canvas.width = this.cols;
    this.canvas.height = this.rows;
    this.pre.style.fontFamily = this.fontFamily;
    this.pre.style.fontSize = this.fontSize + 'px';
    this.pre.style.margin = '0';
    this.pre.style.padding = '0';
    this.pre.style.lineHeight = '1em';
    this.pre.style.position = 'absolute';
    this.pre.style.left = '0';
    this.pre.style.top = '0';
    this.pre.style.zIndex = '9';
    this.pre.style.color = this.glyphColor; // single brand color, no hue cycling
    this.pre.style.backgroundAttachment = 'fixed';
    this.pre.style.mixBlendMode = 'difference';
  }

  render(scene, camera) {
    this.renderer.render(scene, camera);
    const w = this.canvas.width;
    const h = this.canvas.height;
    this.context.clearRect(0, 0, w, h);
    if (this.context && w && h) {
      this.context.drawImage(this.renderer.domElement, 0, 0, w, h);
    }
    this.asciify(this.context, w, h);
    // hue-rotation intentionally removed: keep a single brand accent.
  }

  asciify(ctx, w, h) {
    if (!w || !h) return;
    const imgData = ctx.getImageData(0, 0, w, h).data;
    let str = '';
    for (let y = 0; y < h; y++) {
      for (let x = 0; x < w; x++) {
        const i = x * 4 + y * 4 * w;
        const r = imgData[i], g = imgData[i + 1], b = imgData[i + 2], a = imgData[i + 3];
        if (a === 0) { str += ' '; continue; }
        const gray = (0.3 * r + 0.6 * g + 0.1 * b) / 255;
        let idx = Math.floor((1 - gray) * (this.charset.length - 1));
        if (this.invert) idx = this.charset.length - idx - 1;
        str += this.charset[idx];
      }
      str += '\n';
    }
    this.pre.innerHTML = str;
  }

  dispose() {}
}

class AT_CanvasTxt {
  constructor(txt, opts) {
    opts = opts || {};
    this.canvas = document.createElement('canvas');
    this.context = this.canvas.getContext('2d');
    this.txt = txt;
    this.fontSize = opts.fontSize || 200;
    this.fontFamily = opts.fontFamily || 'Space Mono';
    this.color = opts.color || '#F6FAFF';
    this.font = '600 ' + this.fontSize + 'px ' + this.fontFamily;
  }
  resize() {
    this.context.font = this.font;
    const metrics = this.context.measureText(this.txt);
    const textWidth = Math.ceil(metrics.width) + 20;
    const textHeight =
      Math.ceil(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent) + 20;
    this.canvas.width = textWidth;
    this.canvas.height = textHeight;
  }
  render() {
    this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
    this.context.fillStyle = this.color;
    this.context.font = this.font;
    const metrics = this.context.measureText(this.txt);
    const yPos = 10 + metrics.actualBoundingBoxAscent;
    this.context.fillText(this.txt, 10, yPos);
  }
  get width() { return this.canvas.width; }
  get height() { return this.canvas.height; }
  get texture() { return this.canvas; }
}

class AT_CanvAscii {
  constructor(opts, containerElem, width, height, reduced) {
    this.textString = opts.text;
    this.asciiFontSize = opts.asciiFontSize;
    this.textFontSize = opts.textFontSize;
    this.textColor = opts.textColor;
    this.glyphColor = opts.glyphColor;
    this.planeBaseHeight = opts.planeBaseHeight;
    this.enableWaves = reduced ? false : opts.enableWaves;
    this.reduced = reduced;
    this.container = containerElem;
    this.width = width;
    this.height = height;

    this.camera = new THREE.PerspectiveCamera(45, this.width / this.height, 1, 1000);
    this.camera.position.z = 30;
    this.scene = new THREE.Scene();
    this.mouse = { x: this.width / 2, y: this.height / 2 };
    this.onMouseMove = this.onMouseMove.bind(this);
  }

  async init() {
    try {
      await document.fonts.load('600 200px "Space Mono"');
      await document.fonts.load('400 12px "Space Mono"');
      await document.fonts.ready;
    } catch (e) { /* fallback to system mono */ }
    this.setMesh();
    this.setRenderer();
  }

  setMesh() {
    this.textCanvas = new AT_CanvasTxt(this.textString, {
      fontSize: this.textFontSize,
      fontFamily: 'Space Mono',
      color: this.textColor
    });
    this.textCanvas.resize();
    this.textCanvas.render();

    this.texture = new THREE.CanvasTexture(this.textCanvas.texture);
    this.texture.minFilter = THREE.NearestFilter;

    const textAspect = this.textCanvas.width / this.textCanvas.height;
    const baseH = this.planeBaseHeight;
    const planeW = baseH * textAspect;
    const planeH = baseH;

    this.geometry = new THREE.PlaneGeometry(planeW, planeH, 36, 36);
    this.material = new THREE.ShaderMaterial({
      vertexShader: AT_vertexShader,
      fragmentShader: AT_fragmentShader,
      transparent: true,
      uniforms: {
        uTime: { value: 0 },
        mouse: { value: 1.0 },
        uTexture: { value: this.texture },
        uEnableWaves: { value: this.enableWaves ? 1.0 : 0.0 }
      }
    });
    this.mesh = new THREE.Mesh(this.geometry, this.material);
    this.scene.add(this.mesh);
  }

  setRenderer() {
    this.renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });
    this.renderer.setPixelRatio(1);
    this.renderer.setClearColor(0x000000, 0);

    this.filter = new AT_AsciiFilter(this.renderer, {
      fontFamily: "'Space Mono', 'Courier New', monospace",
      fontSize: this.asciiFontSize,
      glyphColor: this.glyphColor,
      invert: true
    });

    this.container.appendChild(this.filter.domElement);
    this.setSize(this.width, this.height);

    if (!this.reduced) {
      this.container.addEventListener('mousemove', this.onMouseMove);
      this.container.addEventListener('touchmove', this.onMouseMove, { passive: true });
    }
  }

  setSize(w, h) {
    this.width = w;
    this.height = h;
    this.camera.aspect = w / h;
    this.camera.updateProjectionMatrix();
    this.filter.setSize(w, h);
    this.center = { x: w / 2, y: h / 2 };
  }

  load() {
    if (this.reduced) { this.render(); return; }
    this.animate();
  }

  onMouseMove(evt) {
    const e = evt.touches ? evt.touches[0] : evt;
    const bounds = this.container.getBoundingClientRect();
    this.mouse = { x: e.clientX - bounds.left, y: e.clientY - bounds.top };
  }

  animate() {
    const loop = () => {
      this.animationFrameId = requestAnimationFrame(loop);
      this.render();
    };
    loop();
  }

  render() {
    const time = new Date().getTime() * 0.001;
    this.textCanvas.render();
    this.texture.needsUpdate = true;
    this.mesh.material.uniforms.uTime.value = Math.sin(time);
    if (!this.reduced) this.updateRotation();
    this.filter.render(this.scene, this.camera);
  }

  updateRotation() {
    const x = Math.map(this.mouse.y, 0, this.height, 0.5, -0.5);
    const y = Math.map(this.mouse.x, 0, this.width, -0.5, 0.5);
    this.mesh.rotation.x += (x - this.mesh.rotation.x) * 0.05;
    this.mesh.rotation.y += (y - this.mesh.rotation.y) * 0.05;
  }

  clear() {
    this.scene.traverse(obj => {
      if (obj.isMesh && obj.material) {
        if (obj.material.dispose) obj.material.dispose();
        if (obj.geometry && obj.geometry.dispose) obj.geometry.dispose();
      }
    });
    this.scene.clear();
  }

  dispose() {
    cancelAnimationFrame(this.animationFrameId);
    if (this.filter) {
      this.filter.dispose();
      if (this.filter.domElement.parentNode) {
        this.container.removeChild(this.filter.domElement);
      }
    }
    this.container.removeEventListener('mousemove', this.onMouseMove);
    this.container.removeEventListener('touchmove', this.onMouseMove);
    this.clear();
    if (this.renderer) {
      this.renderer.dispose();
      if (this.renderer.forceContextLoss) this.renderer.forceContextLoss();
    }
  }
}

function AsciiText(props) {
  const {
    text = 'hello_world',
    asciiFontSize = 8,
    textFontSize = 200,
    textColor = '#F6FAFF',
    glyphColor = '#A0C8FF',
    planeBaseHeight = 8,
    enableWaves = true
  } = props || {};

  const { useEffect, useRef } = React;
  const containerRef = useRef(null);

  useEffect(() => {
    if (typeof THREE === 'undefined' || !containerRef.current) return;
    const container = containerRef.current;
    const reduced = atReducedMotion();
    let instance = null;
    let ro = null;
    let cancelled = false;

    const w = container.clientWidth || 1;
    const h = container.clientHeight || 1;
    instance = new AT_CanvAscii(
      { text, asciiFontSize, textFontSize, textColor, glyphColor, planeBaseHeight, enableWaves },
      container, w, h, reduced
    );
    instance.init().then(() => {
      if (cancelled) { instance.dispose(); return; }
      instance.load();
    });

    if (!reduced) {
      let rRaf = 0;
      ro = new ResizeObserver(() => {
        cancelAnimationFrame(rRaf);
        rRaf = requestAnimationFrame(() => {
          if (instance && container.clientWidth && container.clientHeight) {
            instance.setSize(container.clientWidth, container.clientHeight);
          }
        });
      });
      ro.observe(container);
    }

    return () => {
      cancelled = true;
      if (ro) ro.disconnect();
      if (instance) instance.dispose();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [text, asciiFontSize, textFontSize, textColor, glyphColor, planeBaseHeight, enableWaves]);

  return React.createElement('div', {
    ref: containerRef,
    className: 'ascii-text-container',
    'aria-label': text.replace(/_/g, ' '),
    role: 'img'
  });
}

window.AsciiText = AsciiText;
