// src/helpers.jsx — small shared utilities

// Counter — animates 0 → target on first scroll-into-view.
// If `display` is provided (a string), renders that statically and skips animation —
// useful for ranges like "1千万–1亿+" that aren't a single number.
function Counter({ target, unit, display }) {
  const ref = React.useRef(null);
  const [val, setVal] = React.useState(display ? null : 0);
  const [revealed, setRevealed] = React.useState(false);
  const started = React.useRef(false);

  React.useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => {
        if (!e.isIntersecting || started.current) return;
        started.current = true;
        // Reveal animation (fade/lift) for display strings.
        if (display) { setRevealed(true); return; }
        const start = performance.now();
        const dur = 1500;
        const isDecimal = target % 1 !== 0;
        const tick = (now) => {
          const t = Math.min(1, (now - start) / dur);
          const eased = 1 - Math.pow(1 - t, 3);
          const v = target * eased;
          setVal(isDecimal ? Math.round(v * 10) / 10 : Math.round(v));
          if (t < 1) requestAnimationFrame(tick);
        };
        requestAnimationFrame(tick);
      });
    }, { threshold: 0.5 });
    io.observe(el);
    return () => io.disconnect();
  }, [target, display]);

  if (display) {
    return <span ref={ref} className={`count-display${revealed ? " in" : ""}`}>{display}</span>;
  }
  const formatted = (typeof val === "number" && target >= 1000 && target % 1 === 0)
    ? val.toLocaleString("en-US")
    : val;
  return <span ref={ref}>{formatted}<span className="unit">{unit}</span></span>;
}

// Reveal-on-scroll wrapper
function Reveal({ children, delay = 0, as: As = "div", className = "", ...rest }) {
  const ref = React.useRef(null);
  React.useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => {
        if (e.isIntersecting) {
          setTimeout(() => el.classList.add("in"), delay);
          io.disconnect();
        }
      });
    }, { threshold: 0.12 });
    io.observe(el);
    return () => io.disconnect();
  }, [delay]);
  return <As ref={ref} className={`reveal ${className}`} {...rest}>{children}</As>;
}

// Smart title that supports [{em:"word"},{br:true}, "text"] mixed
function RichTitle({ parts }) {
  return parts.map((p, i) => {
    if (typeof p === "string") return <React.Fragment key={i}>{p}</React.Fragment>;
    if (p.em) return <em key={i}>{p.em}</em>;
    if (p.br) return <br key={i} />;
    return null;
  });
}

// Stagger — apply progressive entrance delays to a container's direct children.
// Attaches to the wrapped child via cloneElement so it doesn't introduce an
// extra DOM box that would break grid/flex layout.
function Stagger({ children, step = 60, initial = 0 }) {
  const child = React.Children.only(children);
  const ref = React.useRef(null);
  React.useEffect(() => {
    const el = ref.current;
    if (!el) return undefined;
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => {
        if (!e.isIntersecting) return;
        Array.from(el.children).forEach((c, i) => {
          c.style.setProperty("--stagger-delay", `${initial + i * step}ms`);
          c.classList.add("stagger-in");
        });
        io.disconnect();
      });
    }, { threshold: 0.1 });
    io.observe(el);
    return () => io.disconnect();
  }, [step, initial]);
  return React.cloneElement(child, {
    ref,
    className: `${child.props.className || ""} stagger-host`.trim(),
  });
}

function isPublishedItem(item) {
  if (!item) return false;
  if (item.status === "draft" || item.status === "hidden") return false;
  if (item.publishStatus === "draft" || item.publishStatus === "hidden") return false;
  const publishAt = item.publishAt || item.publishedAt;
  if (!publishAt) return true;
  const time = new Date(publishAt).getTime();
  return Number.isNaN(time) || time <= Date.now();
}

function publicItems(items) {
  return Array.isArray(items) ? items.filter(isPublishedItem) : [];
}

// SectionIndex — animated 0X slot-flip for section eyebrows.
// Renders like "§ 03" with a brief roll from random digits → target.
function SectionIndex({ n, label }) {
  const ref = React.useRef(null);
  const [shown, setShown] = React.useState("00");
  const target = String(n).padStart(2, "0");
  React.useEffect(() => {
    const el = ref.current;
    if (!el) return undefined;
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => {
        if (!e.isIntersecting) return;
        const start = performance.now();
        const dur = 520;
        const tick = (now) => {
          const t = Math.min(1, (now - start) / dur);
          if (t < 1) {
            const rd = String(Math.floor(Math.random() * 99)).padStart(2, "0");
            setShown(rd);
            requestAnimationFrame(tick);
          } else {
            setShown(target);
          }
        };
        requestAnimationFrame(tick);
        io.disconnect();
      });
    }, { threshold: 0.4 });
    io.observe(el);
    return () => io.disconnect();
  }, [n, target]);
  return (
    <span ref={ref} className="sec-tag">
      <span className="sec-tag-num">{shown}</span>
      {label ? <span className="sec-tag-label"> / {label}</span> : null}
    </span>
  );
}

Object.assign(window, { Counter, Reveal, RichTitle, Stagger, SectionIndex, isPublishedItem, publicItems });
