/* ==========================================================================
   Duneva — behaviours
   --------------------------------------------------------------------------
   One file, no dependency, no build step. Each behaviour is a small function
   with its own comment; they are called at the bottom, in order.

     1. splitLines      cuts headings into visual lines, for the staircase
     2. staggerDelays   hands each item in a group its own delay
     3. reveals         shows blocks as they enter the screen
     4. loadIn          plays the hero on load rather than on scroll
     5. scrubbers       drives two clip-paths from the scroll position
     6. navigation      the burger menu under 991px
     7. accordions      animates the height of the <details> answers
     8. slider          the two review arrows, and the spoken position
     9. videoDialog     loads the film only when the play button is pressed

   Anything hidden by these behaviours is hidden in CSS under `html.js`, and
   that class is set by enhance.js. Scripts missing = page fully readable.
   ========================================================================== */

(function () {
  "use strict";

  var reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  /* ------------------------------------------------------------------------
     1. Split headings into lines
     ------------------------------------------------------------------------
     The reference cuts its titles with a paid library; the mechanism is the
     same in twenty lines. Each word is measured, words that share a vertical
     offset belong to the same line, and each line is rewrapped in a mask
     (.line) plus a mover (.line__inner).

     The original text is kept in a data attribute so the split can be redone
     from scratch when the window is resized — a title that reflows from two
     lines to three would otherwise keep the old cuts.
     ---------------------------------------------------------------------- */

  function splitLines(el) {
    if (!el.dataset.lineSource) el.dataset.lineSource = el.innerHTML;
    el.innerHTML = el.dataset.lineSource;

    /* Wrap every word — and keep <br> as an explicit break. */
    var pieces = [];
    Array.prototype.slice.call(el.childNodes).forEach(function (node) {
      if (node.nodeType === Node.TEXT_NODE) {
        node.textContent.split(/\s+/).forEach(function (word) {
          if (!word) return;
          var span = document.createElement("span");
          span.textContent = word;
          span.className = "word";
          pieces.push(span);
        });
      } else if (node.nodeName === "BR") {
        pieces.push(document.createElement("br"));
      } else {
        pieces.push(node.cloneNode(true));
      }
    });

    el.innerHTML = "";
    pieces.forEach(function (piece, i) {
      el.appendChild(piece);
      if (i < pieces.length - 1 && piece.nodeName !== "BR") el.appendChild(document.createTextNode(" "));
    });

    /* Group the words by vertical position. */
    var lines = [];
    var current = null;
    var lastTop = null;
    Array.prototype.slice.call(el.querySelectorAll(".word")).forEach(function (word) {
      var top = word.offsetTop;
      if (lastTop === null || Math.abs(top - lastTop) > 2) {
        current = [];
        lines.push(current);
        lastTop = top;
      }
      current.push(word.textContent);
    });

    if (!lines.length) return;

    el.innerHTML = "";
    lines.forEach(function (words, index) {
      var mask = document.createElement("span");
      mask.className = "line";
      var mover = document.createElement("span");
      mover.className = "line__inner";
      mover.textContent = words.join(" ");
      mover.style.setProperty("--line-delay", (index * 0.16).toFixed(2) + "s");
      mask.appendChild(mover);
      el.appendChild(mask);
    });
  }

  function splitAll() {
    var targets = document.querySelectorAll('[data-reveal="lines"], [data-load="lines"]');
    Array.prototype.forEach.call(targets, splitLines);
  }

  /* ------------------------------------------------------------------------
     2. Stagger delays
     ------------------------------------------------------------------------
     Two flavours, both from the reference:
     - [data-reveal-stagger] on a container: its children come one after the
       other, a fixed 120ms apart.
     - [data-reveal-stagger-cell] on grid cells: the delay restarts at every
       row and follows an easing curve (1 − 0.5^n), so the first gap is wide
       and the following ones tighten. It needs the column count, which only
       the browser knows at the current width — hence the recompute on resize.
     ---------------------------------------------------------------------- */

  function staggerDelays() {
    Array.prototype.forEach.call(document.querySelectorAll("[data-reveal-stagger]"), function (group) {
      Array.prototype.forEach.call(group.children, function (child, i) {
        child.style.setProperty("--reveal-delay", (i * 0.12).toFixed(2) + "s");
      });
    });

    var cells = document.querySelectorAll("[data-reveal-stagger-cell]");
    var byParent = new Map();
    Array.prototype.forEach.call(cells, function (cell) {
      if (!byParent.has(cell.parentNode)) byParent.set(cell.parentNode, []);
      byParent.get(cell.parentNode).push(cell);
    });

    byParent.forEach(function (list, parent) {
      var columns = getComputedStyle(parent).gridTemplateColumns.split(" ").filter(Boolean).length || 1;
      list.forEach(function (cell, i) {
        var step = i % columns;
        var curve = 1 - Math.pow(0.5, step); /* 0, 0.5, 0.75, 0.875… */
        cell.style.setProperty("--reveal-delay", (curve * 0.5).toFixed(3) + "s");
      });
    });
  }

  /* ------------------------------------------------------------------------
     3. Scroll reveals
     ------------------------------------------------------------------------
     The trigger sits at 15% from the bottom of the *screen*, not at a share
     of the element: a block taller than the viewport would never reach a
     percentage threshold and would stay hidden for good.

     The safety net at the end catches anything still hidden when the visitor
     reaches the bottom of the page.
     ---------------------------------------------------------------------- */

  function reveals() {
    var targets = document.querySelectorAll("[data-reveal]");
    if (!targets.length) return;

    if (!("IntersectionObserver" in window)) {
      Array.prototype.forEach.call(targets, function (el) {
        el.classList.add("is-in");
      });
      return;
    }

    var observer = new IntersectionObserver(
      function (entries) {
        entries.forEach(function (entry) {
          if (!entry.isIntersecting) return;
          entry.target.classList.add("is-in");
          observer.unobserve(entry.target);
        });
      },
      { rootMargin: "0px 0px -15% 0px", threshold: 0 }
    );

    Array.prototype.forEach.call(targets, function (el) {
      observer.observe(el);
    });

    /* Safety net: at the foot of the page, nothing is left waiting. */
    window.addEventListener(
      "scroll",
      function () {
        if (window.scrollY + window.innerHeight < document.body.scrollHeight - 4) return;
        Array.prototype.forEach.call(document.querySelectorAll("[data-reveal]:not(.is-in)"), function (el) {
          el.classList.add("is-in");
        });
      },
      { passive: true }
    );
  }

  /* ------------------------------------------------------------------------
     4. The hero, on load
     ------------------------------------------------------------------------
     The hero is on screen from the first frame, so it plays on load. The
     delay of each block is written in the HTML (data-load-delay) and read by
     the stylesheet as a multiplier.
     ---------------------------------------------------------------------- */

  function loadIn() {
    var targets = document.querySelectorAll("[data-load]");
    Array.prototype.forEach.call(targets, function (el) {
      el.style.setProperty("--load-delay", el.dataset.loadDelay || 0);
    });
    requestAnimationFrame(function () {
      requestAnimationFrame(function () {
        Array.prototype.forEach.call(targets, function (el) {
          el.classList.add("is-loaded");
        });
      });
    });
  }

  /* ------------------------------------------------------------------------
     5. Scroll-driven clip paths
     ------------------------------------------------------------------------
     Two effects, one mechanism: a 0→1 progress written into a custom
     property while the block crosses the screen. The reference does this
     with a scrubbed timeline smoothed over 0.8s; the same softness comes
     here from interpolating towards the target instead of jumping to it.

     Both properties default to 0 in CSS, which is the *open* state: a page
     without JavaScript shows the photos and the frame wide open.
     ---------------------------------------------------------------------- */

  var SCRUBS = [
    { selector: '[data-scrub="curtain"]', property: "--curtain", from: 0.9, to: 0.3 },
    { selector: '[data-scrub="frame"]', property: "--frame", from: 0.8, to: 0.2 },
  ];

  function scrubbers() {
    if (reduced) return;

    var items = [];
    SCRUBS.forEach(function (config) {
      Array.prototype.forEach.call(document.querySelectorAll(config.selector), function (el) {
        items.push({ el: el, config: config, value: 1 });
      });
    });
    if (!items.length) return;

    var running = false;

    function frame() {
      var height = window.innerHeight;
      var moving = false;

      items.forEach(function (item) {
        var top = item.el.getBoundingClientRect().top;
        var span = (item.config.from - item.config.to) * height;
        var progress = (item.config.from * height - top) / span;
        var target = 1 - Math.max(0, Math.min(1, progress));
        item.value += (target - item.value) * 0.12; /* ≈ 0.8s to catch up */
        if (Math.abs(target - item.value) > 0.001) moving = true;
        else item.value = target;
        item.el.style.setProperty(item.config.property, item.value.toFixed(4));
      });

      if (moving) requestAnimationFrame(frame);
      else running = false;
    }

    function start() {
      if (running) return;
      running = true;
      requestAnimationFrame(frame);
    }

    window.addEventListener("scroll", start, { passive: true });
    window.addEventListener("resize", start);
    start();
  }

  /* ------------------------------------------------------------------------
     6. Navigation
     ---------------------------------------------------------------------- */

  function navigation() {
    var toggle = document.querySelector("[data-nav-toggle]");
    var menu = document.querySelector("[data-nav]");
    if (!toggle || !menu) return;

    function close() {
      menu.classList.remove("is-open");
      toggle.setAttribute("aria-expanded", "false");
      toggle.setAttribute("aria-label", "Open menu");
    }

    toggle.addEventListener("click", function () {
      var open = toggle.getAttribute("aria-expanded") === "true";
      if (open) {
        close();
        return;
      }
      menu.classList.add("is-open");
      toggle.setAttribute("aria-expanded", "true");
      toggle.setAttribute("aria-label", "Close menu");
    });

    /* Following a link closes the panel; so does Escape. */
    menu.addEventListener("click", function (event) {
      if (event.target.closest("a")) close();
    });

    document.addEventListener("keydown", function (event) {
      if (event.key === "Escape") close();
    });
  }

  /* ------------------------------------------------------------------------
     7. Accordions
     ------------------------------------------------------------------------
     <details> already opens and closes on its own — that is why it is used.
     The script only animates the height, and it has to: a closed <details>
     is hidden with content-visibility, so @keyframes and @starting-style
     inside it never replay after the first open.
     ---------------------------------------------------------------------- */

  function accordions() {
    Array.prototype.forEach.call(document.querySelectorAll("[data-accordion]"), function (item) {
      var summary = item.querySelector("summary");
      var answer = item.querySelector(".faq__answer");
      if (!summary || !answer || reduced) return;

      var animation = null;

      summary.addEventListener("click", function (event) {
        event.preventDefault();
        if (animation) animation.cancel();

        var closing = item.open;
        var height = answer.offsetHeight;

        if (!closing) {
          item.open = true;
          height = answer.offsetHeight;
        }

        answer.style.overflow = "hidden";
        animation = answer.animate(
          closing
            ? [{ height: height + "px", opacity: 1 }, { height: "0px", opacity: 0 }]
            : [{ height: "0px", opacity: 0 }, { height: height + "px", opacity: 1 }],
          { duration: 380, easing: "cubic-bezier(0.25, 1, 0.5, 1)" }
        );

        animation.onfinish = function () {
          answer.style.overflow = "";
          if (closing) item.open = false;
          animation = null;
        };
      });
    });
  }

  /* ------------------------------------------------------------------------
     8. Review slider
     ------------------------------------------------------------------------
     The track is a scroll-snap container: it already drags, swipes and takes
     the keyboard. The buttons only push it one card further, and the spoken
     position is read from the scroll offset rather than from a counter, so
     it stays right after a swipe.
     ---------------------------------------------------------------------- */

  function slider() {
    Array.prototype.forEach.call(document.querySelectorAll("[data-slider]"), function (root) {
      var track = root.querySelector("[data-slider-track]");
      var status = root.querySelector("[data-slider-status]");
      var previous = root.querySelector("[data-slider-prev]");
      var next = root.querySelector("[data-slider-next]");
      if (!track) return;

      var total = track.children.length;

      function step(direction) {
        track.scrollBy({ left: direction * track.clientWidth, behavior: reduced ? "auto" : "smooth" });
      }

      if (previous) previous.addEventListener("click", function () { step(-1); });
      if (next) next.addEventListener("click", function () { step(1); });

      track.addEventListener(
        "scroll",
        function () {
          if (!status || !track.clientWidth) return;
          var index = Math.round(track.scrollLeft / track.clientWidth) + 1;
          var text = "Review " + Math.min(index, total) + " of " + total;
          if (status.textContent !== text) status.textContent = text;
        },
        { passive: true }
      );
    });
  }

  /* ------------------------------------------------------------------------
     9. The film
     ------------------------------------------------------------------------
     preload="none" plus a src set on the first click: a visitor who never
     presses play never downloads the 226 KB.
     ---------------------------------------------------------------------- */

  function videoDialog() {
    var dialog = document.querySelector("[data-video-dialog]");
    var open = document.querySelector("[data-video-open]");
    var close = document.querySelector("[data-video-close]");
    if (!dialog || !open) return;

    var player = dialog.querySelector("video");

    open.addEventListener("click", function () {
      if (player && !player.src) player.src = player.dataset.videoSrc;
      if (typeof dialog.showModal === "function") dialog.showModal();
      else dialog.setAttribute("open", "");
      if (player) player.play().catch(function () { /* the browser may refuse: the controls are there */ });
    });

    function shut() {
      if (player) player.pause();
      if (typeof dialog.close === "function") dialog.close();
      else dialog.removeAttribute("open");
    }

    if (close) close.addEventListener("click", shut);
    dialog.addEventListener("click", function (event) {
      if (event.target === dialog) shut(); /* click on the backdrop */
    });
    dialog.addEventListener("close", function () {
      if (player) player.pause();
    });
  }

  /* ------------------------------------------------------------------------
     10. Newsletter
     ------------------------------------------------------------------------
     There is no back end behind a static template, and a strict CSP forbids
     form-action: mailto:. So the entry is turned into a pre-filled message
     for the visitor's own mail app, which is the closest honest equivalent.
     Point the form at your provider and delete this function.
     ---------------------------------------------------------------------- */

  function newsletter() {
    var form = document.querySelector("[data-newsletter]");
    if (!form) return;
    var done = form.querySelector("[data-newsletter-done]");

    form.addEventListener("submit", function (event) {
      var field = form.querySelector("input[type=email]");
      if (!field || !field.value) return;
      event.preventDefault();
      window.location.href =
        "mailto:info@duneva.example?subject=" +
        encodeURIComponent("Trip enquiry") +
        "&body=" +
        encodeURIComponent("Please write to me at " + field.value + ".");
      if (done) done.hidden = false;
    });
  }

  /* ------------------------------------------------------------------------
     Start
     ---------------------------------------------------------------------- */

  function ready(run) {
    if (document.readyState !== "loading") run();
    else document.addEventListener("DOMContentLoaded", run);
  }

  ready(function () {
    splitAll();
    staggerDelays();
    reveals();
    loadIn();
    scrubbers();
    navigation();
    accordions();
    slider();
    videoDialog();
    newsletter();

    /* Fonts land after the first paint and can change where lines break. */
    if (document.fonts && document.fonts.ready) {
      document.fonts.ready.then(function () {
        splitAll();
      });
    }

    /* Recut the titles and recompute the column delays on resize, but only
       when the width actually changed: on mobile, scrolling fires resize. */
    var lastWidth = window.innerWidth;
    var timer = null;
    window.addEventListener("resize", function () {
      if (window.innerWidth === lastWidth) return;
      lastWidth = window.innerWidth;
      clearTimeout(timer);
      timer = setTimeout(function () {
        splitAll();
        staggerDelays();
        Array.prototype.forEach.call(document.querySelectorAll("[data-reveal].is-in"), function (el) {
          el.classList.add("is-in");
        });
      }, 200);
    });
  });
})();
