// spiral-animation.jsx — Ported from TypeScript (spiral-animation.tsx)
// GSAP loaded via CDN global. No imports needed.

// ─── Vector helpers ──────────────────────────────────────────────────────────
class Vector2D {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  static random(min, max) {
    return min + Math.random() * (max - min);
  }
}

class Vector3D {
  constructor(x, y, z) {
    this.x = x;
    this.y = y;
    this.z = z;
  }
  static random(min, max) {
    return min + Math.random() * (max - min);
  }
}

// ─── Star ────────────────────────────────────────────────────────────────────
class Star {
  constructor(cameraZ, cameraTravelDistance) {
    this.angle = Math.random() * Math.PI * 2;
    this.distance = 30 * Math.random() + 15;
    this.rotationDirection = Math.random() > 0.5 ? 1 : -1;
    this.expansionRate = 1.2 + Math.random() * 0.8;
    this.finalScale = 0.7 + Math.random() * 0.6;

    this.dx = this.distance * Math.cos(this.angle);
    this.dy = this.distance * Math.sin(this.angle);

    this.spiralLocation = (1 - Math.pow(1 - Math.random(), 3.0)) / 1.3;
    this.z = Vector2D.random(0.5 * cameraZ, cameraTravelDistance + cameraZ);

    const lerp = (s, e, t) => s * (1 - t) + e * t;
    this.z = lerp(this.z, cameraTravelDistance / 2, 0.3 * this.spiralLocation);
    this.strokeWeightFactor = Math.pow(Math.random(), 2.0);
  }

  render(p, ctrl) {
    const spiralPos = ctrl.spiralPath(this.spiralLocation);
    const q = p - this.spiralLocation;

    if (q > 0) {
      const disp = ctrl.constrain(4 * q, 0, 1);

      const linearE = disp;
      const elasticE = ctrl.easeOutElastic(disp);
      const powerE = Math.pow(disp, 2);

      let easing;
      if (disp < 0.3) {
        easing = ctrl.lerp(linearE, powerE, disp / 0.3);
      } else if (disp < 0.7) {
        const t = (disp - 0.3) / 0.4;
        easing = ctrl.lerp(powerE, elasticE, t);
      } else {
        easing = elasticE;
      }

      let screenX, screenY;

      if (disp < 0.3) {
        screenX = ctrl.lerp(spiralPos.x, spiralPos.x + this.dx * 0.3, easing / 0.3);
        screenY = ctrl.lerp(spiralPos.y, spiralPos.y + this.dy * 0.3, easing / 0.3);
      } else if (disp < 0.7) {
        const mp = (disp - 0.3) / 0.4;
        const curve = Math.sin(mp * Math.PI) * this.rotationDirection * 1.5;
        const bx = spiralPos.x + this.dx * 0.3;
        const by = spiralPos.y + this.dy * 0.3;
        const tx = spiralPos.x + this.dx * 0.7;
        const ty = spiralPos.y + this.dy * 0.7;
        const px = -this.dy * 0.4 * curve;
        const py = this.dx * 0.4 * curve;
        screenX = ctrl.lerp(bx, tx, mp) + px * mp;
        screenY = ctrl.lerp(by, ty, mp) + py * mp;
      } else {
        const fp = (disp - 0.7) / 0.3;
        const bx = spiralPos.x + this.dx * 0.7;
        const by = spiralPos.y + this.dy * 0.7;
        const targetDist = this.distance * this.expansionRate * 1.5;
        const sa = this.angle + 1.2 * this.rotationDirection * fp * Math.PI;
        const tx = spiralPos.x + targetDist * Math.cos(sa);
        const ty = spiralPos.y + targetDist * Math.sin(sa);
        screenX = ctrl.lerp(bx, tx, fp);
        screenY = ctrl.lerp(by, ty, fp);
      }

      const vx = (this.z - ctrl._cameraZ) * screenX / ctrl._viewZoom;
      const vy = (this.z - ctrl._cameraZ) * screenY / ctrl._viewZoom;

      const position = new Vector3D(vx, vy, this.z);

      let sizeMul = 1.0;
      if (disp < 0.6) {
        sizeMul = 1.0 + disp * 0.2;
      } else {
        const t = (disp - 0.6) / 0.4;
        sizeMul = 1.2 * (1 - t) + this.finalScale * t;
      }

      ctrl.showProjectedDot(position, 8.5 * this.strokeWeightFactor * sizeMul);
    }
  }
}

// ─── AnimationController ─────────────────────────────────────────────────────
class SpiralController {
  constructor(canvas, ctx, dpr, size) {
    this.canvas = canvas;
    this.ctx = ctx;
    this.dpr = dpr;
    this.size = size;
    this.time = 0;
    this.stars = [];

    // constants (exposed so Star can read them)
    this._changeEventTime = 0.32;
    this._cameraZ = -400;
    this._cameraTravelDistance = 3400;
    this._startDotYOffset = 28;
    this._viewZoom = 100;
    this._numberOfStars = 5000;
    this._trailLength = 80;

    this._seededRandom();
    this._createStars();

    this._tl = gsap.timeline({ repeat: -1 });
    this._tl.to(this, {
      time: 1,
      duration: 15,
      repeat: -1,
      ease: 'none',
      onUpdate: () => this.render(),
    });
  }

  _seededRandom() {
    const orig = Math.random;
    let seed = 1234;
    Math.random = () => {
      seed = (seed * 9301 + 49297) % 233280;
      return seed / 233280;
    };
    this._createStars();
    Math.random = orig;
  }

  _createStars() {
    this.stars = [];
    for (let i = 0; i < this._numberOfStars; i++) {
      this.stars.push(new Star(this._cameraZ, this._cameraTravelDistance));
    }
  }

  ease(p, g) {
    if (p < 0.5) return 0.5 * Math.pow(2 * p, g);
    return 1 - 0.5 * Math.pow(2 * (1 - p), g);
  }

  easeOutElastic(x) {
    const c4 = (2 * Math.PI) / 4.5;
    if (x <= 0) return 0;
    if (x >= 1) return 1;
    return Math.pow(2, -8 * x) * Math.sin((x * 8 - 0.75) * c4) + 1;
  }

  map(value, s1, e1, s2, e2) {
    return s2 + (e2 - s2) * ((value - s1) / (e1 - s1));
  }

  constrain(v, min, max) {
    return Math.min(Math.max(v, min), max);
  }

  lerp(s, e, t) {
    return s * (1 - t) + e * t;
  }

  spiralPath(p) {
    p = this.constrain(1.2 * p, 0, 1);
    p = this.ease(p, 1.8);
    const turns = 6;
    const theta = 2 * Math.PI * turns * Math.sqrt(p);
    const r = 170 * Math.sqrt(p);
    return new Vector2D(r * Math.cos(theta), r * Math.sin(theta) + this._startDotYOffset);
  }

  rotate(v1, v2, p, orientation) {
    const mx = (v1.x + v2.x) / 2;
    const my = (v1.y + v2.y) / 2;
    const dx = v1.x - mx;
    const dy = v1.y - my;
    const angle = Math.atan2(dy, dx);
    const o = orientation ? -1 : 1;
    const r = Math.sqrt(dx * dx + dy * dy);
    const bounce = Math.sin(p * Math.PI) * 0.05 * (1 - p);
    return new Vector2D(
      mx + r * (1 + bounce) * Math.cos(angle + o * Math.PI * this.easeOutElastic(p)),
      my + r * (1 + bounce) * Math.sin(angle + o * Math.PI * this.easeOutElastic(p))
    );
  }

  showProjectedDot(position, sizeFactor) {
    const t2 = this.constrain(this.map(this.time, this._changeEventTime, 1, 0, 1), 0, 1);
    const camZ = this._cameraZ + this.ease(Math.pow(t2, 1.2), 1.8) * this._cameraTravelDistance;

    if (position.z > camZ) {
      const depth = position.z - camZ;
      const x = this._viewZoom * position.x / depth;
      const y = this._viewZoom * position.y / depth;
      const sw = 400 * sizeFactor / depth;
      this.ctx.lineWidth = sw;
      this.ctx.beginPath();
      this.ctx.arc(x, y, 0.5, 0, Math.PI * 2);
      this.ctx.fill();
    }
  }

  _drawStartDot() {
    if (this.time > this._changeEventTime) {
      const dy = this._cameraZ * this._startDotYOffset / this._viewZoom;
      const pos = new Vector3D(0, dy, this._cameraTravelDistance);
      this.showProjectedDot(pos, 2.5);
    }
  }

  _drawTrail(t1) {
    for (let i = 0; i < this._trailLength; i++) {
      const f = this.map(i, 0, this._trailLength, 1.1, 0.1);
      const sw = (1.3 * (1 - t1) + 3.0 * Math.sin(Math.PI * t1)) * f;
      this.ctx.fillStyle = 'white';
      this.ctx.lineWidth = sw;

      const pathTime = t1 - 0.00015 * i;
      const pos = this.spiralPath(pathTime);
      const offset = new Vector2D(pos.x + 5, pos.y + 5);
      const rotated = this.rotate(pos, offset, Math.sin(this.time * Math.PI * 2) * 0.5 + 0.5, i % 2 === 0);
      this.ctx.beginPath();
      this.ctx.arc(rotated.x, rotated.y, sw / 2, 0, Math.PI * 2);
      this.ctx.fill();
    }
  }

  render() {
    const ctx = this.ctx;
    ctx.fillStyle = 'black';
    ctx.fillRect(0, 0, this.size, this.size);
    ctx.save();
    ctx.translate(this.size / 2, this.size / 2);

    const t1 = this.constrain(this.map(this.time, 0, this._changeEventTime + 0.25, 0, 1), 0, 1);
    const t2 = this.constrain(this.map(this.time, this._changeEventTime, 1, 0, 1), 0, 1);

    ctx.rotate(-Math.PI * this.ease(t2, 2.7));
    this._drawTrail(t1);

    ctx.fillStyle = 'white';
    for (const star of this.stars) {
      star.render(t1, this);
    }
    this._drawStartDot();
    ctx.restore();
  }

  pause() { this._tl.pause(); }
  resume() { this._tl.play(); }
  destroy() { this._tl.kill(); }
}

// ─── React Component ──────────────────────────────────────────────────────────
const SpiralAnimation = () => {
  const canvasRef = React.useRef(null);
  const ctrlRef = React.useRef(null);
  const [dims, setDims] = React.useState({ w: window.innerWidth, h: window.innerHeight });

  // resize
  React.useEffect(() => {
    const onResize = () => setDims({ w: window.innerWidth, h: window.innerHeight });
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);

  // init / re-init on resize
  React.useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    const dpr = window.devicePixelRatio || 1;
    const size = Math.max(dims.w, dims.h);

    canvas.width = size * dpr;
    canvas.height = size * dpr;
    canvas.style.width = `${dims.w}px`;
    canvas.style.height = `${dims.h}px`;
    ctx.scale(dpr, dpr);

    if (ctrlRef.current) ctrlRef.current.destroy();
    ctrlRef.current = new SpiralController(canvas, ctx, dpr, size);

    return () => {
      if (ctrlRef.current) { ctrlRef.current.destroy(); ctrlRef.current = null; }
    };
  }, [dims.w, dims.h]);

  return (
    <div style={{ position: 'relative', width: '100%', height: '100%' }}>
      <canvas ref={canvasRef} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }} />
    </div>
  );
};

// ─── Demo wrapper (used standalone, not in main App) ─────────────────────────
const SpiralDemo = () => {
  const [visible, setVisible] = React.useState(false);

  React.useEffect(() => {
    const t = setTimeout(() => setVisible(true), 2000);
    return () => clearTimeout(t);
  }, []);

  return (
    <div style={{ position: 'fixed', inset: 0, width: '100%', height: '100%', overflow: 'hidden', background: '#000' }}>
      <div style={{ position: 'absolute', inset: 0 }}>
        <SpiralAnimation />
      </div>
      <div style={{
        position: 'absolute', left: '50%', top: '50%',
        transform: 'translate(-50%,-50%)', zIndex: 10,
        transition: 'opacity 1.5s ease, transform 1.5s ease',
        opacity: visible ? 1 : 0,
      }}>
        <button
          onClick={() => {}}
          style={{
            color: 'white', fontSize: '1.5rem', letterSpacing: '0.2em',
            textTransform: 'uppercase', fontWeight: 200,
            background: 'none', border: 'none', cursor: 'pointer',
            transition: 'letter-spacing 0.7s',
          }}
        >
          Enter
        </button>
      </div>
    </div>
  );
};

window.SpiralAnimation = SpiralAnimation;
window.SpiralDemo = SpiralDemo;
