/**
 * Rocketship UI JS.
 *
 * Helper functions:
 * - checkScreenSize
 * - getBreakpoint
 * - optimizedResize
 * - scrollTo
 * - getScrollTop
 * - imgLoaded
 * - round
 **/

(function(Drupal, window, document) {
  "use strict";

  // Set namespace for UI javascript
  if (typeof window.rocketshipUI == 'undefined') {
    window.rocketshipUI = {};
  }

  const self = window.rocketshipUI;

  ///////////////////////////////////////////////////////////////////////
  // Cache variables available across the namespace
  ///////////////////////////////////////////////////////////////////////

  self.html = document.querySelector('html');
  self.body = document.querySelector('body');
  self.page = document.scrollingElement || document.documentElement;
  self.touch = false;
  self.screen = '';
  self.scrollStop = false;

  ///////////////////////////////////////////////////////////////////////
  // Behavior for Base: triggers
  ///////////////////////////////////////////////////////////////////////

  Drupal.behaviors.rocketshipUIHelpers = {
    attach: (context, settings) => {
      // Find out our current breakpoint.
      // saves it in a variable 'screen'.
      self.checkScreenSize();

      window.rocketshipUI.optimizedResize().add(() => {
        self.checkScreenSize();
      });

      // Add passiveSupported check for use with adding events
      self.checkPassiveSupported();
    }
  };

  ///////////////////////////////////////////////////////////////////////
  // Helper functions
  ///////////////////////////////////////////////////////////////////////

  /**
   * Add passiveSupported check for use with adding events.
   */
  self.checkPassiveSupported = () => {
    self.passiveSupported = false;

    try {
      var options = {
        get passive() {
          // This function will be called when the browser
          // attempts to access the passive property.
          self.passiveSupported = true;
        }
      };

      window.addEventListener("test", options, options);
      window.removeEventListener("test", options, options);
    } catch (err) {
      self.passiveSupported = false;
    }
  };

  /**
   * Find out if we're on a small device (phone).
   */
  self.checkScreenSize = () => {
    var currentBreakpoint = self.getBreakpoint();

    switch (currentBreakpoint) {
      case 'bp-xs':
        self.screen = 'xs';
        break;

      case 'bp-sm':
        self.screen = 'sm';
        break;

      case 'bp-md':
        self.screen = 'md';
        break;

      case 'bp-lg':
        self.screen = 'lg';
        break;
    }
  };

  /**
   * Get the current breakpoint.
   * Refers to the content of the body::after pseudo-element (set in set-breakpoints.scss)
   * call with window.rocketshipUI.getBreakpoint().
   */
  self.getBreakpoint = () => {
    var tag = window.getComputedStyle(document.body, '::after').getPropertyValue('content');
    // Firefox bugfix
    tag = tag.replace(/"/g, '');

    return tag.replace(/'/g, '');
  };

  /**
   * Debounce function so event handlers don't get called too many times
   * when fired in quick succession
   *
   * https://davidwalsh.name/javascript-debounce-function
   *
   * @param func
   * @param wait
   * @param immediate
   * @returns {Function}
   */
  // Example usage:
  //
  // var mouseupHandler = self.debounce(function(e) {
  //   // do stuff
  // }, 250);
  // document.body.addEventListener('mouseup', mouseupHandler, self.passiveSupported ? {capture: false, once: false, passive: true} : false);
  self.debounce = (func, wait, immediate) => {
    var timeout;
    return () => {
      var context = this,
        args = arguments;
      var later = () => {
        timeout = null;
        if (!immediate) func.apply(context, args);
      };
      var callNow = immediate && !timeout;
      clearTimeout(timeout);
      timeout = setTimeout(later, wait);
      if (callNow) func.apply(context, args);
    };
  };

  /**
   * Since resize events can fire at a high rate,
   * the event handler shouldn't execute computationally expensive operations
   * such as DOM modifications.
   * Instead, it is recommended to throttle the event using requestAnimationFrame,
   * setTimeout or customEvent
   *
   * Src: https://developer.mozilla.org/en-US/docs/Web/Events/resize
   *
   * Example:
   *
   * window.rocketshipUI.optimizedResize().add(() => {
   *   // do something
   * });
   */
  self.optimizedResize = () => {
    var callbacks = [],
      running = false;

    // Fired on resize event.
    function resize() {
      if (!running) {
        running = true;
        if (window.requestAnimationFrame) {
          window.requestAnimationFrame(runCallbacks);
        }
        else {
          setTimeout(runCallbacks, 250);
        }
      }
    }

    // Run the actual callbacks.
    function runCallbacks() {
      callbacks.forEach((callback) => {
        callback();
      });
      running = false;
    }

    // Adds callback to loop.
    function addCallback(callback) {
      if (callback) {
        callbacks.push(callback);
      }
    }
    return {
      // Public method to add additional callback.
      add: (callback) => {
        if (!callbacks.length) {
          window.addEventListener('resize', resize);
        }
        addCallback(callback);
      }
    };
  };

  /**
   * Function to scroll smoothly to an anchor in the page.
   *
   * @el = required!, element to scroll to.
   * @offset = not required, offset the landing position or set to 'bottom' to scroll to bottom of the element.
   * @speed = not required, speed with which to scroll.
   * @callback = callback function that can be invoked after scroll to is done.
   */
  self.scrollTo = (params) => {
    const scrollEvents = ['scroll', 'mousedown', 'wheel', 'DOMMouseScroll', 'mousewheel', 'keyup', 'touchmove'];
    params.pos = params.el.offsetTop;

    if (typeof params.offset === 'undefined') {
      params.offset = 0;
    }
    if (params.offset === 'bottom') {
      params.pos = params.el.offsetTop + params.el.offsetHeight;
    }
    if (typeof params.speed === 'undefined') {
      params.speed = 1000;
    }
    if (typeof params.callback === 'undefined') {
      params.callback = () => {};
    }

    // When user does any of these events, cancel all running animated scrolls.
    for (const event of scrollEvents) {
      self.page.addEventListener(event, self.scrollStop);
    }

    function animateScroll() {
      const start = self.page.scrollTop;
      const startTime = 'now' in window.performance ? performance.now() : new Date().getTime();

      function scroll() {
        const currentTime = 'now' in window.performance ? performance.now() : new Date().getTime();
        const time = Math.min(1, ((currentTime - startTime) / params.speed));

        self.page.scrollTop = Math.ceil((time * (params.pos + params.offset - start)) + start);

        if (self.page.scrollTop === params.pos + params.offset) {
          cancelAnimationFrame(animation);
          params.callback();
          for (const event of scrollEvents) {
            self.page.removeEventListener(event, self.scrollStop);
          }
        } else {
          requestAnimationFrame(scroll);
        }
      }

      requestAnimationFrame(scroll);
    }

    animateScroll();
  };

  /**
   * Cancels a running scrollTo call.
   *
   * https://stackoverflow.com/questions/18445590/jquery-animate-stop-scrolling-when-user-scrolls-manually#18445654
   */
  self.scrollStop = () => {
    // remove queued animation + don't complete current animation =>
    // abrupt end of the scroll.
    self.page.scrollTop = self.page.scrollTop;
  };

  /**
   * Get the top scroll position.
   */
  self.getScrollTop = () => {
    return window.pageYOffset || document.documentElement.scrollTop;
  };

  /**
   * Detect if all the images withing your object are loaded.
   *
   * No longer needs imagesLoaded plugin to work.
   */
  self.imgLoaded = (el, callback) => {
    var img = el.querySelectorAll('img'),
      iLength = img.length,
      iCount = 0;

    if (iLength) {
      img.forEach((image) => {
        // fires after images are loaded (if not cached)
        image.addEventListener('load', () => {
          iCount = iCount + 1;

          if (iCount === iLength) {
            // all images loaded so proceed
            callback();
          }
        });

        // in case images are cached
        // re-enter the load function in order to get to the callback.
        if (image.complete) {
          image.addEventListener('load', () => {
            iCount = iCount + 1;

            if (iCount === iLength) {
              // all images loaded so proceed
              callback();
            }
          });
          image.dispatchEvent(new Event('load'));
        }
      });
    }
    else {
      // no images, so we can proceed
      return callback();
    }
  };

  /**
   * Round numbers to x decimals.
   *
   * http://www.jacklmoore.com/notes/rounding-in-javascript/
   */
  self.round = (value, decimals) => {
    return Number(Math.round(value + 'e' + decimals) + 'e-' + decimals);
  };
})(Drupal, window, document);

// Global javascript (loaded on all pages in Pattern Lab and Drupal)
// Should be used sparingly because javascript files can be used in components.

// JavaScript should be made compatible with libraries other than jQuery by
// wrapping it with an "anonymous closure". See:
// - https://drupal.org/node/1446420
// - http://www.adequatelygood.com/2010/3/JavaScript-Module-Pattern-In-Depth

/**
 * Rocketship UI JS
 *
 * Contains: triggers for functions.
 * Functions themselves are split off and grouped below each behavior.
 */
(function (Drupal, once, window, document) {
  "use strict";
  // Set namespace for frontend UI javascript.
  if (typeof window.rocketshipUI === 'undefined') {
    window.rocketshipUI = {};
  }
  const self = window.rocketshipUI;

  ///////////////////////////////////////////////////////////////////////
  // Triggers
  ///////////////////////////////////////////////////////////////////////
  Drupal.behaviors.rocketshipUIBase = {
    attach: (context) => {
      const tabs = context.querySelector('.tabs__nav');
      const teasers = context.querySelectorAll('.fully-clickable');
      // Add a class to body to indicate the page has edit tabs
      // so we can set it fixed if needed.
      if (tabs) {
        document.body.classList.add('has-tabs');
      }
      // Smooth scrolling to anchors
      // True if fixed header, false for regular header.
      self.scrollToAnchor(0);
      const skipLink = document.querySelector('a[href="#main-content"]');
      const content = document.querySelector('#main-content');
      if (skipLink && content) {
        // When you click on the skiplink, force focus on the target.
        once('skip-link', [skipLink]).forEach((linkElement) => {
          linkElement.addEventListener('click', (e) => {
            content.focus();
            e.preventDefault();
          });
        });
      }
      // Make cards clickable: view teasers.
      if (teasers.length) {
        self.cardLink(teasers);
      }
    }
  };

  ///////////////////////////////////////////////////////////////////////
  // Functions
  ///////////////////////////////////////////////////////////////////////

  /**
   * Make a card clickable, if there is a content link in it to reference.
   * If no selector is given, it will try to go for a 'dedicated' link
   * - a 'read more' link
   * - or a button (if there is only 1).
   * Variables:
   * - elements: NodeList referencing an element.
   * - linkSelector: string (optional).
   */
  self.cardLink = (elements, linkSelector, fallbackSelector) => {
    var newTab = false;
    // For each of our elements.
    once('cardlink', elements).forEach((element) => {
      var el = element,
          link, hrefProp, targetProp, down, up;
      // Is a selector was passed to the function.
      // Use it to find the dedicated link to go to.
      if (typeof linkSelector !== 'undefined' && linkSelector.length) {
        link = el.querySelector(linkSelector);
      }
      // If selector not found, look for fallback.
      if (typeof link === 'undefined' || !link) {
        link = el.querySelector(fallbackSelector);
      }
      // If fallback selector found, look for other to use.
      if (typeof link === 'undefined' || !link) {
        link = self.getCardLink(el).link;
      }
      // If we have a link, go ahead.
      if (typeof link !== 'undefined' && link !== null && link) {
        hrefProp = link.getAttribute('href');
        targetProp = link.getAttribute('target');
        // Set cursor on the card.
        el.style.cursor = 'pointer';
        // If there is a dedicated link in the item
        // Make the div clickable
        // (ignore if a child link is clicked, so be sure to check target!).
        once('cardlink-mousedown', [el]).forEach((element) => {
          element.addEventListener('mousedown', (e) => {
            down = +new Date();
            if (e.ctrlKey || e.metaKey) {
              newTab = true;
            }
          });
        });
        once('cardlink-mouseup', [el]).forEach((element) => {
          element.addEventListener('mouseup', (e) => {
            up = +new Date();
            // Only fire if really clicked on element.
            if ((up - down) < 250) {
              var myEl = element;
              // If the target of the click is the el...
              // ... or a descendant of the el
              if (!myEl.is(e.target) || !myEl.contains(e.target)) {
                // If it's a link tag,
                if (e.target.matches('a, a *')) {
                  // do nothing
                  // Else, trigger the dedicated link
                } else {
                  // If new tab key is pressed
                  // Open in new tab.
                  if (newTab) {
                    window.open(hrefProp, '_blank');
                  } else {
                    // If target defined, open in target.
                    if (typeof targetProp !== 'undefined') {
                      // Fire a click event on the original link
                      window.open(hrefProp, targetProp);
                    } else {
                      window.location.href = hrefProp;
                    }
                  }
                }
              }
            }
            // Reset the flags that determine opening the link in a new browser tab.
            newTab = false;
          });
        });
      }
    });
  };

  /**
   * Get the card link.
   *
   * @param {HTMLElement} el The element.
   *
   * @return {object} The link object.
   */
  self.getCardLink = (el) => {
    let link;
    // 'read more'.
    link = el.querySelector('.field--name-node-link a');
    if (link) {
      return {
        link: link
      };
    }
    // singular button
    link = el.querySelector('.field--buttons a');
    if (link && link.length === 1) {
      return {
        link: link
      };
    }
    // other link field
    link = el.querySelector('[class*="field--name-field-link-"] a:last-of-type');
    if (link) {
      return {
        link: link
      };
    }
    return {
      link: null
    };
  };

  /**
   * Scroll to anchor in page.
   *
   * @param {number} offset Number to offset when scrolling to destination.
   */
  self.scrollToAnchor = (offset) => {
    let animatedScrolling = false;
    let exceptions = null;

    if (drupalSettings?.theme_settings?.scroll_to) {
      animatedScrolling = true;
    }

    if (drupalSettings?.theme_settings?.scroll_to_exceptions?.length) {
      exceptions = drupalSettings.theme_settings.scroll_to_exceptions;
    }
    // Only do animated scrolling if set in theme settings.
    if (animatedScrolling) {
      // On page load, check if hash in the url matches with an anchor on page
      // And scroll to it.
      var newOffset,
          tabs = document.querySelector('.tabs'),
          tabsHeight = 0,
          adminHeight = 0,
          header = document.querySelector('.sticky-top'); // Use whatever wrapper you need.
      // Check for fixed elements on top of site
      // To compensate offset of the anchor when scrolling.
      if (document.body.classList.contains('toolbar-fixed')) {
        adminHeight = document.querySelector('#toolbar-bar').offsetHeight;
        if (tabs) {
          tabsHeight = tabs.offsetHeight;
        }
      }
      // Only add offset if fixed header and/or value passed in function.
      if (typeof offset === 'undefined') offset = 0;
      // Function to calculate the offset, in case of fixed header or not.
      var calcOffset = () => {
        if (header && header.style.position === 'fixed') {
          offset = header.offsetHeight + offset + adminHeight + tabsHeight; // Compensate for fixed header height.
          // Compare offset to height of header to see if it's smaller
          // Must at least use the header height as offset or we'll get a gap.
          newOffset = header.offsetHeight;
          if (newOffset > offset) offset = newOffset;
        }
        else {
          // Calculate offset for a normal page (no fixed header).
          offset = 15 + offset + adminHeight + tabsHeight; // Compensate with bit of space above the element with the anchor.
        }
        // Need negative value for the scroll calculations.
        offset = -(offset);
        return offset;
      };
      // Get hash from page url (includes the hash sign).
      var urlHash = window.location.hash;
      if (urlHash && document.querySelector(urlHash)) {
        // Possible anchor links that refer to that hash.
        const anchorLinks = document.querySelectorAll(`a[href$="${ urlHash }"]`);

        // Trigger the scrollTo, only if NO anchorLink selector on the page
        // OR anchorLink selector is not part of exceptions.

        // 1) No anchor link
        if (anchorLinks.length < 1) {
          // Recalculate offset if fixed header.
          offset = calcOffset();

          // El, offset, speed, callback
          var scrollParams = {
            el: document.querySelector(urlHash),
            offset: offset,
            speed: 500
          };

          self.scrollTo(scrollParams);
        }

        // 2) Check all of anchorLinks if there are any + if not an exception
        once('scrollable-anchors-active', anchorLinks).forEach((linkElement) => {
          var myLink = linkElement;

          // Set an active class & scrollTo (if not part of the exclusion list).
          if (exceptions === null || !exceptions.includes(myLink)) {
            // Recalculate offset if fixed header.
            offset = calcOffset();

            // El, offset, speed, callback
            var scrollParams = {
              el: document.querySelector(urlHash),
              offset: offset,
              speed: 500
            };

            self.scrollTo(scrollParams);

            myLink.classList.add('js-active-anchor');
          }
        });
      }

      // When clicking an anchor, animate scroll to that anchor and add to history
      // If that anchor is not excluded.
      var anchorLinks = document.querySelectorAll('a[href*="#"]:not(a[href="#"])');

      once('scrollable-anchors', anchorLinks).forEach((linkElement) => {
        var anchorLink = linkElement;

        if (exceptions === null || !exceptions.includes(anchorLink)) {
          // Remove active classes on anchor links.
          anchorLink.classList.remove('active');
          anchorLink.classList.remove('active-trail');
        }

        // Click anchor link and animate scroll to it.
        once('scrollable-anchor-click', [anchorLink]).forEach((element) => {
          element.addEventListener('click', (e) => {
            // If there are links to ignore for smooth scroll
            // Or anchorLink is not an exception
            if (exceptions === null || !exceptions.includes(anchorLink)) {
              var path = element.href,
                  id, hash, target;

              if (typeof e.target.href !== 'undefined') {
                id = e.target.href.split('#')[1];
              }

              else if (typeof e.currentTarget.href !== 'undefined') {
                id = e.currentTarget.href.split('#')[1];
              }

              hash = `#${ id }`;
              target = document.querySelector(hash);

              // Recalculate offset if fixed header.
              offset = calcOffset();

              if (target) {
                // Set an active class.
                anchorLink.classList.add('js-active-anchor');

                var scrollParamsForTarget = {
                  el: target,
                  offset: offset,
                  speed: 500,
                  callback: () => {
                    // Change url without refresh.
                    document.location.hash = hash;
                  }
                };

                self.scrollTo(scrollParamsForTarget);

                // If supported by the browser we can also update the URL.
                if (window.history && window.history.pushState) {
                  history.pushState("", document.title, hash);
                }
                // If no anchor with id matching href
                // Check for anchor with a name matching the href.
              }
              else {
                var newTarget = document.querySelector('a[name="' + id + '"]');

                if (newTarget) {
                  // Set an active class.
                  anchorLink.classList.add('js-active-anchor');

                  var scrollParamsForNewTarget = {
                    el: newTarget,
                    offset: offset,
                    speed: 500,
                    callback: () => {
                      // Change url without refresh.
                      document.location.hash = hash;
                    }
                  };

                  self.scrollTo(scrollParamsForNewTarget);
                }
              }

              // The url, minus stuff after hash or parameters.
              var currentUrl = window.location.href.split(/[?#]/)[0];
              // The path, minus stuff after hash or parameters.
              var pathBase = path.split(/[?#]/)[0];

              // If the URLs (stripped of hashes and parameters) don't match up, it's
              // a link to a different page.
              // Don't scroll to anchor on same page on click.
              if (currentUrl.replace(/\/$/, "") !== pathBase.replace(/\/$/, "")) {
                return true;
              }
              // We're on the same page and don't need a page reload.
              e.preventDefault();
            }
          });
        });
      });

      // When going back/forward in history
      // Make sure the events associated with the anchor animations are fired
      // If that anchor is not excluded.
      window.onpopstate = () => {
        once('scrollable-anchors-popstate', anchorLinks).forEach((linkElement) => {
          const anchorLink = linkElement;

          if (exceptions === null || !exceptions.includes(anchorLink)) {
            const path = linkElement.href;
            // Get hash from page url.
            const urlHash = window.location.hash;
            // Get hash & ID from href.
            const linkHash = path.substring(path.indexOf("#"));

            // Recalculate offset if fixed header.
            offset = calcOffset();

            if (document.querySelector(linkHash)) {
              // If hash of current path matches hash of the url
              // Scroll to it.
              if (urlHash === linkHash) {
                if (exceptions === null || !exceptions.includes(anchorLink)) {
                  anchorLink.classList.add('js-active-anchor');

                  const scrollParams = {
                    el: document.querySelector(linkHash),
                    offset: offset,
                    speed: 500
                  };

                  self.scrollTo(scrollParams);
                }
              }
            }
          }
        });
      };
    }
  };

})(Drupal, once,  window, document);
