/* ==========================================================================
   Augusta Pediatric Associates — Meet Our Doctors marquee

   Lives in its own file because it is a SITE-WIDE feature: the doctors
   carousel appears on the homepage and on all ten pediatrician bio pages,
   while redesign.js is a homepage-only layer (its split-panel rows are keyed
   to the homepage service CTAs). Loading the whole redesign layer onto the bio
   pages to get the marquee would ship a lot of inert machinery to pages it was
   never designed for.

   Column counts differ per page — the homepage runs 4 across, the bio pages 5
   (`data-columns="medium-6 thb-5"`) — so every figure here is measured from
   the DOM rather than configured, and each carousel on the page is driven
   independently.
   ========================================================================== */

(function () {
  'use strict';

  /* ======================================================================
     Meet Our Doctors — a continuous marquee we drive ourselves

     This started as a reconfigured slick autoplay (autoplaySpeed 0, a 10s
     per-card duration, linear ease) so that one move ran into the next. It
     reads fine standing still, but it is not actually a marquee, and every
     problem with it came from that:

       - `slickPause()` stops autoplay SCHEDULING, not the 10s transition
         already in flight, so a grab could not truly interrupt the drift
       - slick is mid-transition permanently, so its `animating` flag is never
         false and `slideHandler` refuses every `slickGoTo` unless
         `waitForAnimate` is off — the release could not resync the index
       - on wrap, slick re-normalises the infinite clones and hard-repositions
         the track, which reads as the carousel sticking and then jumping
       - `pauseOnHover` stops it while you are reaching for it, so the thing
         you are about to grab is already dead

     Patching those one at a time did not converge. So slick keeps doing what
     it is good at — building the slides, the clones and the responsive widths
     — and its autoplay is switched off entirely. A rAF loop owns the track
     transform from then on. That gives a genuinely constant px/s with no
     transition boundaries to hitch on, and the drag becomes trivial because
     it is the same variable: the gesture writes `x`, the loop keeps writing
     `x`, and letting go simply resumes.

     The track is periodic — slick's clones repeat the same run of doctors —
     so wrapping `x` by exactly one set width is seamless with nothing to
     re-normalise.
     ====================================================================== */

  var DOCTOR_PXS = 31.5;      /* px per second. 315px card / 10s, as before. */
  var DRAG_MIN   = 5;         /* px before a press is a drag, not a click     */

  /* The drift used to stop on hover. It no longer does: you cannot grab a
     thing that dies as your cursor approaches it, and a steady pace is the
     point. Set true to restore the old behaviour. */
  var PAUSE_ON_HOVER = false;

  /* ----------------------------------------------------------------------
     The reveal rule, carried by this file rather than by a stylesheet.

     The theme parks every `.animation` card at opacity 0 and lets its
     scroll-reveal add the class that releases it. Re-initialising slick
     rebuilds the whole track, so the clones never carry that class — and
     because the reveal for this section has already run by then, neither do
     the originals. The result is a Meet Our Doctors section that is present,
     laid out, drifting, and completely invisible.

     redesign.css has always carried the override, but redesign.css is loaded
     on the homepage alone, so the ten pediatrician bio pages showed an empty
     band. The rule belongs with the code that causes the problem, which is
     this file, and is injected here so any page that loads the marquee is
     covered without a second <link> to keep in sync. Duplicating the
     homepage rule is harmless: it is identical, and !important against
     !important is settled by source order onto the same value.
     ---------------------------------------------------------------------- */
  function ensureRevealOverride() {
    if (document.getElementById("apa-marquee-reveal")) return;
    var el = document.createElement("style");
    el.id = "apa-marquee-reveal";
    el.textContent =
      ".thb-team-row .thb-carousel .thb-team-member.animation {" +
      "opacity: 1 !important; transform: none !important; }";
    (document.head || document.documentElement).appendChild(el);
  }

  function marqueeDoctors() {
    var $ = window.jQuery;
    if (!$ || !$.fn || !$.fn.slick) return false;

    var $all = $('.thb-team-row .thb-carousel');
    if (!$all.length) return false;

    /* Every carousel is converted independently. One per page today, but the
       per-page slick config differs (the homepage runs 4 across, the bio pages
       5), so nothing may be hoisted out of this loop — options, slide widths
       and clone counts are all per-instance. */
    var ready = 0;

    $all.each(function () {
      var $car = $(this);
      if (!$car.hasClass('slick-initialized')) return;   /* theme not done yet */
      if (this.__apaMarquee) { ready++; return; }        /* already converted   */

      var opts = $car.slick('getSlick').options;

      /* Portraits are lazy-loaded, which suited a carousel that sat still
         between steps. This one shows every doctor within a minute and cards
         arrive from the edge rather than by being scrolled to, so a lazy one
         can come in blank. Done before the re-init so the clones inherit it. */
      $car.find('img[loading="lazy"]').attr('loading', 'eager');

      $car.slick('unslick');
      $car.slick($.extend({}, opts, {
        /* Everything that would write to the track is off. The loop below owns
           that property, and two writers would fight every frame. */
        autoplay:      false,
        draggable:     false,
        swipe:         false,
        touchMove:     false,
        infinite:      true,    /* keep the clones - they are what makes it loop */
        dots:          false,   /* they counted pages, and there are none now    */
        arrows:        false
      }));

      driveMarquee($car, $);
      ready++;
    });

    /* Only done once every carousel on the page has been taken over, so the
       retry keeps running if the theme initialised one of them late. */
    return ready === $all.length;
  }

  /* Owns .slick-track's transform for the life of the page. */
  function driveMarquee($car, $) {
    var el = $car[0];
    var track = el.querySelector('.slick-track');
    if (!track) return;

    if (el.__apaMarquee) el.__apaMarquee.stop();

    var x = 0, originX = 0, loopW = 0, step = 0;
    var raf = 0, last = 0, hovering = false;
    var down = false, moved = false, startPx = 0, baseX = 0;

    /* offsetLeft differences, not outerWidth: slick sizes slides inline and
       the gutter may be padding, margin or neither depending on the theme's
       breakpoint. The distance between two slides is the only figure that is
       true in all of them. */
    function measure() {
      var slides = el.querySelectorAll('.slick-slide');
      if (slides.length < 2) return false;

      step = slides[1].offsetLeft - slides[0].offsetLeft;
      if (!step) return false;

      var originals = el.querySelectorAll('.slick-slide:not(.slick-cloned)');
      if (!originals.length) return false;

      loopW = step * originals.length;
      originX = originals[0].offsetLeft;
      return true;
    }

    /* x lives in (-originX - loopW, -originX]. Because the track repeats with
       period loopW, both ends of that range show the identical arrangement,
       so the wrap is invisible. */
    function wrap(v) {
      if (!loopW) return v;
      while (v <= -originX - loopW) v += loopW;
      while (v > -originX) v -= loopW;
      return v;
    }

    function paint() {
      track.style.transform = 'translate3d(' + x.toFixed(2) + 'px, 0px, 0px)';
    }

    function frame(t) {
      var dt = last ? Math.min(t - last, 64) : 16.7;   /* clamp tab-switch spikes */
      last = t;
      if (!down && !(PAUSE_ON_HOVER && hovering)) x = wrap(x - DOCTOR_PXS * dt / 1000);
      paint();
      raf = requestAnimationFrame(frame);
    }

    /* changedTouches as well as touches: on touchend the live list is empty,
       and the release coordinate lives in changedTouches. */
    function pointX(e) {
      if (e.touches && e.touches.length) return e.touches[0].clientX;
      if (e.changedTouches && e.changedTouches.length) return e.changedTouches[0].clientX;
      return e.clientX;
    }

    function onDown(e) {
      if (e.type === 'mousedown' && e.button !== 0) return;
      down = true; moved = false;
      startPx = pointX(e);
      baseX = x;
    }

    function onMove(e) {
      if (!down) return;
      var dx = pointX(e) - startPx;
      if (!moved && Math.abs(dx) < DRAG_MIN) return;
      moved = true;
      x = wrap(baseX + dx);
      /* Only once it is a drag, so a plain click still opens the bio and the
         page can still be scrolled by touch until then. */
      if (e.cancelable) e.preventDefault();
    }

    /* On document, not the carousel: a drag released off the row still has to
       hand control back, or it stays stuck under the pointer. */
    function onUp(e) {
      if (!down) return;
      /* Settle on the RELEASE coordinate, not on the last mousemove we
         happened to receive. Move events are coalesced and the final one is
         routinely dropped, which leaves the track short of where the hand
         actually finished — the gesture then looks like it slipped. */
      if (moved) {
        var dx = pointX(e) - startPx;
        if (dx === dx) { x = wrap(baseX + dx); paint(); }   /* NaN guard; paint so
                                                              the release lands on
                                                              this tick, not next */
      }
      down = false;                 /* the loop resumes drifting from here */
    }

    /* A click that ends a drag must not open that doctor's bio. Capture,
       because the anchor is a descendant and would otherwise act first. */
    function onClick(e) {
      if (moved) { e.preventDefault(); e.stopPropagation(); moved = false; }
    }

    function onEnter() { hovering = true; }
    function onLeave() { hovering = false; }

    /* Slick recomputes slide widths on resize and its own breakpoints, so the
       step and the loop width move with it. Re-measure and re-anchor rather
       than carrying a stale period, which would make the wrap visible. */
    /* Also the setPosition handler, which is the important half. Slick writes
       its own transform whenever it recalculates — several times during the
       first seconds while the portraits load — and that write lands after our
       frame, so the leaked value is what actually paints. Repainting straight
       after it puts our position back within the same tick.

       Re-anchoring is guarded: while the geometry is unchanged there is
       nothing to re-derive, and recomputing the phase on every one of those
       events was itself yanking the track by up to a card. */
    function onResize() {
      var prevStep = step, prevLoop = loopW, prevOrigin = originX;
      var phase = loopW ? (x + originX) / loopW : 0;

      if (!measure()) return;

      if (step !== prevStep || loopW !== prevLoop || originX !== prevOrigin) {
        x = wrap(-originX + phase * loopW);
      }
      paint();
    }

    el.addEventListener('mousedown', onDown);
    el.addEventListener('touchstart', onDown, { passive: true });
    document.addEventListener('mousemove', onMove);
    document.addEventListener('touchmove', onMove, { passive: false });
    document.addEventListener('mouseup', onUp);
    document.addEventListener('touchend', onUp);
    document.addEventListener('touchcancel', onUp);
    el.addEventListener('click', onClick, true);
    el.addEventListener('mouseenter', onEnter);
    el.addEventListener('mouseleave', onLeave);
    $(window).on('resize.apamarquee', onResize);
    $car.on('setPosition.apamarquee', onResize);

    if (!measure()) return;

    /* Slick animates the track by default; every frame we write would be
       tweened toward instead of applied. */
    track.style.transition = 'none';
    x = -originX;
    paint();
    raf = requestAnimationFrame(frame);

    el.__apaMarquee = {
      stop: function () {
        cancelAnimationFrame(raf);
        el.removeEventListener('mousedown', onDown);
        el.removeEventListener('touchstart', onDown);
        document.removeEventListener('mousemove', onMove);
        document.removeEventListener('touchmove', onMove);
        document.removeEventListener('mouseup', onUp);
        document.removeEventListener('touchend', onUp);
        document.removeEventListener('touchcancel', onUp);
        el.removeEventListener('click', onClick, true);
        el.removeEventListener('mouseenter', onEnter);
        el.removeEventListener('mouseleave', onLeave);
        $(window).off('resize.apamarquee');
        $car.off('setPosition.apamarquee');
        el.__apaMarquee = null;
      }
    };
  }

  /* slick is initialised by the theme, which may not have run yet. Try, then
     keep trying briefly rather than guessing at the ordering. */
  function marqueeDoctorsWhenReady() {
    if (marqueeDoctors()) return;

    var tries = 0;
    var timer = setInterval(function () {
      if (marqueeDoctors() || ++tries > 40) clearInterval(timer);
    }, 250);
  }

  function boot() {
    ensureRevealOverride();
    marqueeDoctorsWhenReady();
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', boot);
  } else {
    boot();
  }
})();
