// Shared primitives for the portfolio.

const { useEffect, useRef, useState, useMemo } = React;

// ─── Hooks ───────────────────────────────────────────────────────────────────
function useReveal(threshold = 0) {
  const ref = useRef(null);
  const [shown, setShown] = useState(false);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    // Immediate check: if the element is already in (or near) the viewport
    // at mount, reveal right away. Some browsers/layouts don't fire IO for
    // already-intersecting elements on mount.
    const check = () => {
      const r = el.getBoundingClientRect();
      const vh = window.innerHeight || document.documentElement.clientHeight;
      const vw = window.innerWidth || document.documentElement.clientWidth;
      if (r.bottom > 0 && r.top < vh && r.right > 0 && r.left < vw) {
        setShown(true);
        return true;
      }
      return false;
    };
    if (check()) return;

    let obs;
    try {
      obs = new IntersectionObserver(
        (entries) => {
          for (const e of entries) {
            if (e.isIntersecting || e.intersectionRatio > 0) {
              setShown(true);
              obs.disconnect();
              return;
            }
          }
        },
        { threshold: [0, threshold].filter((v, i, a) => a.indexOf(v) === i), rootMargin: "0px 0px -10% 0px" }
      );
      obs.observe(el);
    } catch (e) { setShown(true); }

    // Belt-and-braces: re-check on scroll/resize in case IO is misbehaving.
    const onScroll = () => { if (check() && obs) obs.disconnect(); };
    window.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("resize", onScroll);
    return () => {
      if (obs) obs.disconnect();
      window.removeEventListener("scroll", onScroll);
      window.removeEventListener("resize", onScroll);
    };
  }, []);
  return [ref, shown];
}

function useTypewriter(words, speed = 80, pause = 1400) {
  const [text, setText] = useState("");
  const [i, setI] = useState(0);
  const [del, setDel] = useState(false);
  useEffect(() => {
    const cur = words[i % words.length];
    if (!del && text === cur) {
      const t = setTimeout(() => setDel(true), pause);
      return () => clearTimeout(t);
    }
    if (del && text === "") {
      setDel(false); setI(v => v + 1); return;
    }
    const t = setTimeout(() => {
      setText(del ? cur.slice(0, text.length - 1) : cur.slice(0, text.length + 1));
    }, del ? speed / 2 : speed);
    return () => clearTimeout(t);
  }, [text, del, i, words, speed, pause]);
  return text;
}

// ─── Reveal wrapper ──────────────────────────────────────────────────────────
function Reveal({ children, delay = 0, y = 24, as = "div", className = "", style = {} }) {
  const [ref, shown] = useReveal();
  const Tag = as;
  return (
    <Tag
      ref={ref}
      className={className}
      style={{
        ...style,
        opacity: shown ? 1 : 0,
        transform: shown ? "none" : `translate3d(0,${y}px,0)`,
        transition: `opacity .8s cubic-bezier(.2,.7,.2,1) ${delay}ms, transform .9s cubic-bezier(.2,.7,.2,1) ${delay}ms`,
        willChange: "opacity, transform",
      }}
    >
      {children}
    </Tag>
  );
}

// ─── Particle canvas (hero background) ───────────────────────────────────────
function ParticleField({ color = "#7c5cff", count = 60, speed = 0.4, link = true, style = {} }) {
  const ref = useRef(null);
  useEffect(() => {
    const c = ref.current; if (!c) return;
    const ctx = c.getContext("2d");
    let w, h, parts, raf;
    const resize = () => {
      const r = c.getBoundingClientRect();
      w = c.width = r.width * devicePixelRatio;
      h = c.height = r.height * devicePixelRatio;
    };
    const init = () => {
      parts = Array.from({ length: count }, () => ({
        x: Math.random() * w, y: Math.random() * h,
        vx: (Math.random() - 0.5) * speed * devicePixelRatio,
        vy: (Math.random() - 0.5) * speed * devicePixelRatio,
        r: (Math.random() * 1.8 + 0.6) * devicePixelRatio,
      }));
    };
    const tick = () => {
      ctx.clearRect(0, 0, w, h);
      for (const p of parts) {
        p.x += p.vx; p.y += p.vy;
        if (p.x < 0 || p.x > w) p.vx *= -1;
        if (p.y < 0 || p.y > h) p.vy *= -1;
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
        ctx.fillStyle = color;
        ctx.globalAlpha = 0.8;
        ctx.fill();
      }
      if (link) {
        ctx.globalAlpha = 1;
        for (let i = 0; i < parts.length; i++) {
          for (let j = i + 1; j < parts.length; j++) {
            const dx = parts[i].x - parts[j].x;
            const dy = parts[i].y - parts[j].y;
            const d = Math.hypot(dx, dy);
            const max = 120 * devicePixelRatio;
            if (d < max) {
              ctx.strokeStyle = color;
              ctx.globalAlpha = (1 - d / max) * 0.25;
              ctx.lineWidth = 0.6 * devicePixelRatio;
              ctx.beginPath();
              ctx.moveTo(parts[i].x, parts[i].y);
              ctx.lineTo(parts[j].x, parts[j].y);
              ctx.stroke();
            }
          }
        }
      }
      raf = requestAnimationFrame(tick);
    };
    resize(); init(); tick();
    const ro = new ResizeObserver(() => { resize(); init(); });
    ro.observe(c);
    return () => { cancelAnimationFrame(raf); ro.disconnect(); };
  }, [color, count, speed, link]);
  return <canvas ref={ref} style={{ width: "100%", height: "100%", display: "block", ...style }} />;
}

// ─── Custom cursor ───────────────────────────────────────────────────────────
function CustomCursor({ accent = "#7c5cff", enabled = true }) {
  const dot = useRef(null);
  const ring = useRef(null);
  useEffect(() => {
    if (!enabled) {
      document.body.classList.remove("cursor-hidden");
      return;
    }
    document.body.classList.add("cursor-hidden");
    let x = 0, y = 0, rx = 0, ry = 0;
    let raf;
    const onMove = (e) => { x = e.clientX; y = e.clientY; };
    const onOver = (e) => {
      const hov = e.target.closest("a,button,.hov,input,textarea,[data-hov]");
      if (ring.current) ring.current.dataset.hov = hov ? "1" : "0";
    };
    const tick = () => {
      rx += (x - rx) * 0.18; ry += (y - ry) * 0.18;
      if (dot.current) dot.current.style.transform = `translate(${x}px, ${y}px)`;
      if (ring.current) ring.current.style.transform = `translate(${rx}px, ${ry}px)`;
      raf = requestAnimationFrame(tick);
    };
    window.addEventListener("mousemove", onMove);
    window.addEventListener("mouseover", onOver);
    raf = requestAnimationFrame(tick);
    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("mouseover", onOver);
      document.body.classList.remove("cursor-hidden");
    };
  }, [enabled]);
  if (!enabled) return null;
  return (
    <React.Fragment>
      <div ref={dot} style={{
        position: "fixed", left: -4, top: -4, width: 8, height: 8, borderRadius: "50%",
        background: accent, pointerEvents: "none", zIndex: 9999, mixBlendMode: "difference",
      }} />
      <div ref={ring} className="ring" style={{
        position: "fixed", left: -20, top: -20, width: 40, height: 40, borderRadius: "50%",
        border: `1.5px solid ${accent}`, pointerEvents: "none", zIndex: 9998,
        transition: "width .25s, height .25s, margin .25s, border-color .25s",
      }} />
      <style>{`
        .ring[data-hov="1"]{width:64px;height:64px;margin:-12px;border-width:2px;background:${accent}22}
      `}</style>
    </React.Fragment>
  );
}

Object.assign(window, {
  useReveal, useTypewriter, Reveal, ParticleField, CustomCursor,
});
