/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* eslint no-shadow: ["error", { "allow": ["top"] }] */

/**
 * Scroll the document so that the element "elem" appears in the viewport.
 *
 * @param {DOMNode} elem
 *        The element that needs to appear in the viewport.
 * @param {boolean} centered
 *        true if you want it centered, false if you want it to appear on the
 *        top of the viewport. It is true by default, and that is usually what
 *        you want.
 * @param {boolean} smooth
 *        true if you want the scroll to happen smoothly, instead of instantly.
 *        It is false by default.
 */
function scrollIntoViewIfNeeded(elem, centered = true, smooth = false) {
  const win = elem.ownerDocument.defaultView;
  const clientRect = elem.getBoundingClientRect();

  // The following are always from the {top, bottom}
  // of the viewport, to the {top, …} of the box.
  // Think of them as geometrical vectors, it helps.
  // The origin is at the top left.

  const topToBottom = clientRect.bottom;
  const bottomToTop = clientRect.top - win.innerHeight;
  // We allow one translation on the y axis.
  let yAllowed = true;

  // disable smooth scrolling when the user prefers reduced motion
  const reducedMotion = win.matchMedia("(prefers-reduced-motion)").matches;
  smooth = smooth && !reducedMotion;

  const options = { behavior: smooth ? "smooth" : "auto" };

  // Whatever `centered` is, the behavior is the same if the box is
  // (even partially) visible.
  if ((topToBottom > 0 || !centered) && topToBottom <= elem.offsetHeight) {
    win.scrollBy(
      Object.assign({ left: 0, top: topToBottom - elem.offsetHeight }, options)
    );
    yAllowed = false;
  } else if (
    (bottomToTop < 0 || !centered) &&
    bottomToTop >= -elem.offsetHeight
  ) {
    win.scrollBy(
      Object.assign({ left: 0, top: bottomToTop + elem.offsetHeight }, options)
    );

    yAllowed = false;
  }

  // If we want it centered, and the box is completely hidden,
  // then we center it explicitly.
  if (centered) {
    if (yAllowed && (topToBottom <= 0 || bottomToTop >= 0)) {
      const x = win.scrollX;
      const y =
        win.scrollY +
        clientRect.top -
        (win.innerHeight - elem.offsetHeight) / 2;
      win.scroll(Object.assign({ left: x, top: y }, options));
    }
  }
}

function closestScrolledParent(node) {
  if (node == null) {
    return null;
  }

  const window = node.ownerDocument?.defaultView || node.defaultView;
  // Typically ignore Document when reaching the top-most element
  const isElement = node instanceof window.HTMLElement;

  // Ensure that the scrolled parent can actually scroll.
  // In the debugger's scope panel, the "div.accordion" has scrollable content
  // but "div.secondary-panes" is the parent element with overflow:auto
  const overflowY = isElement ? window.getComputedStyle(node).overflowY : null;
  const isScrollable = overflowY !== "visible" && overflowY !== "hidden";

  if (isScrollable && node.scrollHeight > node.clientHeight) {
    return node;
  }

  return closestScrolledParent(node.parentNode);
}

/**
 * Scrolls the element into view if it is not visible.
 *
 * @param {DOMNode|undefined} element
 *        The item to be scrolled to.
 *
 * @param {object | undefined} options
 *        An options object which can contain:
 *          - container: possible scrollable container. If it is not scrollable, we will
 *                       look it up.
 *          - alignTo:   "top" or "bottom" to indicate if we should scroll the element
 *                       to the top or the bottom of the scrollable container when the
 *                       element is off canvas.
 *          - center:    Indicate if we should scroll the element to the middle of the
 *                       scrollable container when the element is off canvas.
 */
function scrollIntoView(element, options = {}) {
  if (!element) {
    return;
  }

  const { alignTo, center, container } = options;

  const { top, bottom } = element.getBoundingClientRect();
  const scrolledParent = closestScrolledParent(container || element.parentNode);
  const scrolledParentRect = scrolledParent
    ? scrolledParent.getBoundingClientRect()
    : null;
  const isVisible =
    !scrolledParent ||
    (top >= scrolledParentRect.top && bottom <= scrolledParentRect.bottom);

  if (isVisible) {
    return;
  }

  if (center) {
    element.scrollIntoView({ block: "center" });
    return;
  }

  const scrollToTop = alignTo
    ? alignTo === "top"
    : !scrolledParentRect || top < scrolledParentRect.top;
  element.scrollIntoView(scrollToTop);
}

// Exports from this module
export { scrollIntoViewIfNeeded, scrollIntoView };
