/* =========================================================
   TRUE SHAPE SALON — Interações e animações
   01. Utilitários
   02. Split text (palavras / caracteres)
   03. Revelação por scroll
   04. Header (esconder/mostrar + fundo)
   05. Navegação mobile
   06. Marquee (duplicação da faixa)
   07. Especialidades (sticky scroll)
   08. Parallax
   09. Cursor personalizado
   10. Diversos (ano, nav ativa, back to top)
   11. Banner e consentimento de cookies
   12. Rolagem suave
   ========================================================= */

(function () {
  "use strict";

  /* ---------- 01. Utilitários ---------- */
  const $ = (sel, ctx) => (ctx || document).querySelector(sel);
  const $$ = (sel, ctx) => Array.from((ctx || document).querySelectorAll(sel));
  const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  const raf = (fn) => {
    let ticking = false;
    return function () {
      if (ticking) return;
      ticking = true;
      requestAnimationFrame(() => {
        fn();
        ticking = false;
      });
    };
  };

  /* ---------- 02. Split text ---------- */
  // Divide o texto em <span> por palavra ou caractere e aplica um atraso
  // incremental — base das revelações do hero (palavras) e dos subtítulos
  // das páginas internas (caracteres), como no original.
  function split(el) {
    const mode = el.dataset.split;
    const step = parseInt(el.dataset.stagger || (mode === "chars" ? 18 : 70), 10);
    const frag = document.createDocumentFragment();
    let i = 0;

    const splitText = (text) => {
      text.split(/(\s+)/).forEach((chunk) => {
        if (chunk === "") return;
        if (/^\s+$/.test(chunk)) {
          frag.appendChild(document.createTextNode(" "));
          return;
        }

        if (mode === "chars") {
          // agrupa por palavra para não partir palavras no fim da linha
          const word = document.createElement("span");
          word.style.display = "inline-block";
          word.style.whiteSpace = "nowrap";
          chunk.split("").forEach((ch) => {
            const s = document.createElement("span");
            s.className = "split-char";
            s.style.setProperty("--d", i * step + "ms");
            s.textContent = ch;
            word.appendChild(s);
            i++;
          });
          frag.appendChild(word);
        } else {
          const s = document.createElement("span");
          s.className = "split-word";
          s.style.setProperty("--d", i * step + "ms");
          s.textContent = chunk;
          frag.appendChild(s);
          i++;
        }
      });
    };

    // percorre os nós filhos para preservar os <br> escritos no HTML
    Array.from(el.childNodes).forEach((node) => {
      if (node.nodeType === 1 && node.tagName === "BR") {
        frag.appendChild(document.createElement("br"));
      } else {
        splitText(node.textContent);
      }
    });

    el.textContent = "";
    el.appendChild(frag);
  }

  $$("[data-split]").forEach(split);

  /* ---------- 03. Revelação por scroll ---------- */
  // Verificação geométrica simples em vez de IntersectionObserver: o rácio de
  // interseção pode ficar a zero quando o elemento está mascarado ou sem área
  // resolvida, e nesse caso a imagem nunca chegava a aparecer. Aqui só conta a
  // posição da caixa, que é sempre fiável. A lista vai-se esvaziando à medida
  // que os elementos são revelados, por isso o custo cai para zero.
  const revealTargets = $$("[data-reveal], [data-split]");

  // atraso escalonado automático em grupos (grelhas, listas de preços)
  $$("[data-stagger-group]").forEach((group) => {
    const step = parseInt(group.dataset.staggerGroup, 10) || 80;
    $$("[data-reveal]", group).forEach((child, i) => {
      child.style.setProperty("--d", i * step + "ms");
    });
  });

  if (reduced) {
    revealTargets.forEach((el) => el.classList.add("is-revealed"));
  } else {
    // o que está acima da dobra (hero) revela logo no arranque
    let pending = revealTargets.filter((el) => {
      if (el.closest(".hero, .page-hero")) {
        requestAnimationFrame(() => el.classList.add("is-revealed"));
        return false;
      }
      return true;
    });

    const checkReveal = () => {
      const limit = window.innerHeight * 0.92;
      pending = pending.filter((el) => {
        const r = el.getBoundingClientRect();
        if (r.top < limit && r.bottom > -1) {
          el.classList.add("is-revealed");
          return false;
        }
        return true;
      });
      if (!pending.length) {
        window.removeEventListener("scroll", onReveal);
        window.removeEventListener("resize", onReveal);
      }
    };

    const onReveal = raf(checkReveal);

    window.addEventListener("scroll", onReveal, { passive: true });
    window.addEventListener("resize", onReveal);
    window.addEventListener("load", onReveal);
    checkReveal();
  }

  /* ---------- 04. Header ---------- */
  const header = $(".header");
  if (header) {
    let last = window.scrollY;
    const onScroll = raf(() => {
      const y = window.scrollY;
      header.classList.toggle("is-stuck", y > 24);
      const navOpen = $(".nav.is-open");
      if (!navOpen) {
        header.classList.toggle("is-hidden", y > last && y > 320);
      }
      last = y;
    });
    window.addEventListener("scroll", onScroll, { passive: true });
    onScroll();
  }

  /* ---------- 05. Navegação mobile ---------- */
  const toggle = $(".nav-toggle");
  const nav = $(".nav");
  if (toggle && nav) {
    toggle.addEventListener("click", () => {
      // Nota: não se bloqueia o scroll com overflow:hidden no body —
      // isso cria um scroll container que parte o position:fixed no mobile.
      const open = nav.classList.toggle("is-open");
      toggle.classList.toggle("is-open", open);
      toggle.setAttribute("aria-expanded", String(open));
      // marca o header: o menu tem fundo claro, por isso nas páginas de hero
      // escuro é preciso desfazer a inversão de cor enquanto está aberto
      header.classList.toggle("nav-open", open);
      if (open) header.classList.remove("is-hidden");
    });

    $$(".nav__link", nav).forEach((link) => {
      link.addEventListener("click", () => {
        nav.classList.remove("is-open");
        toggle.classList.remove("is-open");
        toggle.setAttribute("aria-expanded", "false");
        header.classList.remove("nav-open");
      });
    });

    // fecha com Escape
    document.addEventListener("keydown", (e) => {
      if (e.key === "Escape" && nav.classList.contains("is-open")) toggle.click();
    });
  }

  /* ---------- 06. Marquee ---------- */
  // Duplica o conteúdo da faixa para o loop de -50% ficar contínuo.
  $$(".marquee__track").forEach((track) => {
    track.innerHTML = track.innerHTML + track.innerHTML;
  });

  /* ---------- 07. Especialidades (sticky scroll) ---------- */
  // A secção tem altura de N ecrãs; o progresso do scroll define a categoria
  // ativa e faz o crossfade da imagem correspondente.
  const spec = $(".spec");
  if (spec) {
    const items = $$(".spec__item", spec);
    const media = $$(".spec__media img", spec);
    const total = items.length;
    let current = -1;

    const setActive = (idx) => {
      if (idx === current) return;
      current = idx;
      items.forEach((it, i) => it.classList.toggle("is-active", i === idx));
      media.forEach((m, i) => m.classList.toggle("is-active", i === idx));
    };

    const update = raf(() => {
      const rect = spec.getBoundingClientRect();
      const scrollable = rect.height - window.innerHeight;
      if (scrollable <= 0) return;
      const progress = Math.min(Math.max(-rect.top / scrollable, 0), 0.9999);
      setActive(Math.floor(progress * total));
    });

    // tem de coincidir com o breakpoint do CSS (max-width: 980px),
    // senão a secção ganha 400vh de altura sem o sticky ativo
    const desktop = window.matchMedia("(min-width: 981px)");

    const setup = () => {
      if (desktop.matches) {
        spec.style.height = total * 100 + "vh";
        update();
      } else {
        spec.style.height = "";
        current = -1;
        setActive(0);
      }
    };

    window.addEventListener("scroll", () => { if (desktop.matches) update(); }, { passive: true });
    window.addEventListener("resize", setup);
    setup();

    // hover também troca a imagem (desktop)
    items.forEach((item, i) => {
      item.addEventListener("mouseenter", () => {
        if (desktop.matches) setActive(i);
      });
    });
  }

  /* ---------- 08. Parallax ---------- */
  const parallaxEls = $$("[data-parallax]");
  if (parallaxEls.length && !reduced) {
    const update = raf(() => {
      const vh = window.innerHeight;
      parallaxEls.forEach((el) => {
        const rect = el.getBoundingClientRect();
        if (rect.bottom < -200 || rect.top > vh + 200) return;
        const speed = parseFloat(el.dataset.parallax) || 0.12;
        const center = rect.top + rect.height / 2 - vh / 2;
        el.style.transform = `translate3d(0, ${(-center * speed).toFixed(2)}px, 0)`;
      });
    });
    window.addEventListener("scroll", update, { passive: true });
    window.addEventListener("resize", update);
    update();
  }

  /* ---------- 09. Cursor personalizado ---------- */
  const fine = window.matchMedia("(hover: hover) and (pointer: fine)").matches;
  if (fine && !reduced) {
    const cursor = document.createElement("div");
    cursor.className = "cursor";
    cursor.innerHTML = '<div class="cursor__ring"></div><div class="cursor__dot"></div>';
    document.body.appendChild(cursor);
    document.body.classList.add("has-cursor");

    const ring = $(".cursor__ring", cursor);
    const dot = $(".cursor__dot", cursor);
    let mx = window.innerWidth / 2, my = window.innerHeight / 2;
    let rx = mx, ry = my;

    document.addEventListener("mousemove", (e) => {
      mx = e.clientX;
      my = e.clientY;
      cursor.classList.add("is-visible");
      dot.style.transform = `translate3d(${mx}px, ${my}px, 0)`;
    });

    document.addEventListener("mouseleave", () => cursor.classList.remove("is-visible"));

    (function loop() {
      rx += (mx - rx) * 0.16;
      ry += (my - ry) * 0.16;
      ring.style.transform = `translate3d(${rx.toFixed(2)}px, ${ry.toFixed(2)}px, 0)`;
      requestAnimationFrame(loop);
    })();

    const hoverSel = 'a, button, .price-row, .spec__item, [data-cursor="hover"]';
    document.addEventListener("mouseover", (e) => {
      if (e.target.closest(hoverSel)) cursor.classList.add("is-hover");
    });
    document.addEventListener("mouseout", (e) => {
      if (e.target.closest(hoverSel) && !e.relatedTarget?.closest?.(hoverSel)) {
        cursor.classList.remove("is-hover");
      }
    });
  }

  /* ---------- 10. Diversos ---------- */
  // ano dinâmico no footer
  $$("[data-year]").forEach((el) => {
    el.textContent = new Date().getFullYear();
  });

  // marca o link ativo da navegação
  const page = document.body.dataset.page;
  if (page) {
    $$(".nav__link").forEach((link) => {
      if (link.dataset.nav === page) link.setAttribute("aria-current", "page");
    });
  }

  // back to top
  $$("[data-top]").forEach((el) => {
    el.addEventListener("click", (e) => {
      e.preventDefault();
      window.scrollTo({ top: 0, behavior: reduced ? "auto" : "smooth" });
    });
  });

  /* ---------- 11. Banner e consentimento de cookies ---------- */
  // Guarda a escolha em localStorage. Os scripts de análise/marketing só
  // devem ser carregados quando window.trueShapeConsent.analytics for true
  // (ouvir o evento "ts:consent" para reagir a mudanças).
  const CONSENT_KEY = "ts_cookie_consent";
  const banner = $(".cookie-banner");

  const readConsent = () => {
    try {
      return JSON.parse(localStorage.getItem(CONSENT_KEY) || "null");
    } catch (err) {
      return null;
    }
  };

  const applyConsent = (consent) => {
    window.trueShapeConsent = consent;
    document.dispatchEvent(new CustomEvent("ts:consent", { detail: consent }));
  };

  const saveConsent = (analytics, marketing) => {
    const consent = {
      necessary: true,
      analytics: !!analytics,
      marketing: !!marketing,
      date: new Date().toISOString()
    };
    try {
      localStorage.setItem(CONSENT_KEY, JSON.stringify(consent));
    } catch (err) {
      /* modo privado / storage bloqueado — a escolha vale só para esta sessão */
    }
    applyConsent(consent);
    if (banner) banner.classList.remove("is-open");
  };

  const stored = readConsent();
  if (stored) applyConsent(stored);

  if (banner) {
    const prefs = $(".cookie-prefs", banner);
    const openBanner = () => {
      banner.classList.add("is-open");
      banner.removeAttribute("hidden");
    };

    if (!stored) {
      banner.removeAttribute("hidden");
      setTimeout(openBanner, 700);
    }

    banner.addEventListener("click", (e) => {
      const action = e.target.closest("[data-cookie]");
      if (!action) return;
      e.preventDefault();

      switch (action.dataset.cookie) {
        case "accept":
          saveConsent(true, true);
          break;
        case "reject":
          saveConsent(false, false);
          break;
        case "customize":
          if (prefs) prefs.classList.toggle("is-open");
          break;
        case "save":
          saveConsent(
            $("#cookie-analytics")?.checked,
            $("#cookie-marketing")?.checked
          );
          break;
      }
    });

    // reabrir as definições a partir do rodapé ou das páginas legais
    $$('[data-cookie="open"]').forEach((el) => {
      el.addEventListener("click", (e) => {
        e.preventDefault();
        const current = readConsent();
        const analytics = $("#cookie-analytics");
        const marketing = $("#cookie-marketing");
        if (analytics) analytics.checked = !!current?.analytics;
        if (marketing) marketing.checked = !!current?.marketing;
        if (prefs) prefs.classList.add("is-open");
        openBanner();
      });
    });
  }

  /* ---------- 12. Rolagem suave ---------- */
  // Interpola a posição de scroll da própria janela (window.scrollTo), em vez de
  // transformar um wrapper: assim o header fixed e a secção sticky continuam a
  // funcionar. Só em rato; no touch fica a rolagem nativa do sistema.
  if (!reduced && fine) {
    const EASE = 0.1;
    let target = window.scrollY;
    let position = target;
    let animating = false;

    const maxScroll = () =>
      Math.max(0, document.documentElement.scrollHeight - window.innerHeight);

    // painéis com scroll próprio (tabelas das páginas legais) mantêm o comportamento normal
    const inScrollable = (node) => {
      let el = node instanceof Element ? node : null;
      while (el && el !== document.body) {
        const s = getComputedStyle(el);
        if (/(auto|scroll)/.test(s.overflowY) && el.scrollHeight > el.clientHeight + 1) return true;
        el = el.parentElement;
      }
      return false;
    };

    const loop = () => {
      position += (target - position) * EASE;

      if (Math.abs(target - position) < 0.4) {
        position = target;
        animating = false;
        window.scrollTo({ top: Math.round(position), behavior: "instant" });
        return;
      }

      window.scrollTo({ top: Math.round(position), behavior: "instant" });
      requestAnimationFrame(loop);
    };

    window.addEventListener(
      "wheel",
      (e) => {
        if (e.ctrlKey || e.defaultPrevented) return; // zoom do browser
        if (inScrollable(e.target)) return;

        e.preventDefault();

        // normaliza os modos de delta (pixel / linha / página)
        const delta =
          e.deltaMode === 1 ? e.deltaY * 16 :
          e.deltaMode === 2 ? e.deltaY * window.innerHeight :
          e.deltaY;

        target = Math.min(Math.max(target + delta, 0), maxScroll());

        if (!animating) {
          animating = true;
          position = window.scrollY;
          requestAnimationFrame(loop);
        }
      },
      { passive: false }
    );

    // scroll vindo de outra origem (teclado, âncoras, barra) volta a sincronizar
    window.addEventListener(
      "scroll",
      () => {
        if (!animating) {
          target = window.scrollY;
          position = target;
        }
      },
      { passive: true }
    );
  }
})();
