import * as doms from "../../shared/utils/dom";

const SCROLL_DELTA_X = 64;
const SCROLL_DELTA_Y = 64;

// dirty way to store scrolling state on globally
let scrolling = false;
let lastTimeoutId: number | null = null;

// Check if an element is scrollable by actually scrolling it.
// Only checks for vertical scrollability.
// Credit: https://github.com/philc/vimium/blob/bdf654aebe6f570f427c5f7bc9592cad86e642b5/content_scripts/scroller.js#L93
const canBeScrolled = (element: Element): boolean => {
  const scrollTopBefore = element.scrollTop;
  element.scrollBy(0, 1);
  const scrollTopAfter = element.scrollTop;
  element.scrollBy(0, -1);
  return scrollTopBefore + 1 === scrollTopAfter;
};

// Check if the element's overflow and visibility permit scrolling.
// Credit: https://github.com/philc/vimium/blob/bdf654aebe6f570f427c5f7bc9592cad86e642b5/content_scripts/scroller.js#L74
const isScrollableStyle = (element: Element): boolean => {
  const {
    overflowX,
    overflowY,
    overflow,
    visibility,
  } = window.getComputedStyle(element);
  if ([overflow, overflowX, overflowY].includes("hidden")) {
    return false;
  }
  if (["hidden", "collapse"].includes(visibility)) {
    return false;
  }
  return canBeScrolled(element);
};

// Find a visible and scrollable element by depth-first search.  Currently
// this method is called by each scrolling, and the returned value of this
// method is not cached.  That does not cause performance issue because in the
// most pages, the window is root element i,e, documentElement.
const findScrollable = (element: Element): Element | null => {
  if (isScrollableStyle(element)) {
    return element;
  }

  const children = Array.from(element.children).filter(doms.isVisible);
  for (const child of children) {
    const scrollable = findScrollable(child);
    if (scrollable) {
      return scrollable;
    }
  }
  return null;
};

const scrollTarget = () => {
  if (
    window.document.scrollingElement &&
    isScrollableStyle(window.document.scrollingElement)
  ) {
    return window.document.scrollingElement;
  }

  const target = findScrollable(window.document.documentElement);
  if (target) {
    return target;
  }
  return window.document.documentElement;
};

const resetScrolling = () => {
  scrolling = false;
};

class Scroller {
  private element: Element;

  private smooth: boolean;

  constructor(element: Element, smooth: boolean) {
    this.element = element;
    this.smooth = smooth;
  }

  scrollTo(x: number, y: number): void {
    if (!this.smooth) {
      this.element.scrollTo(x, y);
      return;
    }
    this.element.scrollTo({
      left: x,
      top: y,
      behavior: "smooth",
    });
    this.prepareReset();
  }

  scrollBy(x: number, y: number): void {
    const left = this.element.scrollLeft + x;
    const top = this.element.scrollTop + y;
    this.scrollTo(left, top);
  }

  prepareReset(): void {
    scrolling = true;
    if (lastTimeoutId) {
      clearTimeout(lastTimeoutId);
      lastTimeoutId = null;
    }
    lastTimeoutId = window.setTimeout(resetScrolling, 100);
  }
}

export type Point = { x: number; y: number };

export default interface ScrollPresenter {
  getScroll(): Point;
  scrollVertically(amount: number, smooth: boolean): void;
  scrollHorizonally(amount: number, smooth: boolean): void;
  scrollPages(amount: number, smooth: boolean): void;
  scrollTo(x: number, y: number, smooth: boolean): void;
  scrollToTop(smooth: boolean): void;
  scrollToBottom(smooth: boolean): void;
  scrollToHome(smooth: boolean): void;
  scrollToEnd(smooth: boolean): void;
}

export class ScrollPresenterImpl {
  getScroll(): Point {
    const target = scrollTarget();
    return { x: target.scrollLeft, y: target.scrollTop };
  }

  scrollVertically(count: number, smooth: boolean): void {
    const target = scrollTarget();
    let delta = SCROLL_DELTA_Y * count;
    if (scrolling) {
      delta = SCROLL_DELTA_Y * count * 4;
    }
    new Scroller(target, smooth).scrollBy(0, delta);
  }

  scrollHorizonally(count: number, smooth: boolean): void {
    const target = scrollTarget();
    let delta = SCROLL_DELTA_X * count;
    if (scrolling) {
      delta = SCROLL_DELTA_X * count * 4;
    }
    new Scroller(target, smooth).scrollBy(delta, 0);
  }

  scrollPages(count: number, smooth: boolean): void {
    const target = scrollTarget();
    const height = target.clientHeight;
    let delta = height * count;
    if (scrolling) {
      delta = height * count;
    }
    new Scroller(target, smooth).scrollBy(0, delta);
  }

  scrollTo(x: number, y: number, smooth: boolean): void {
    const target = scrollTarget();
    new Scroller(target, smooth).scrollTo(x, y);
  }

  scrollToTop(smooth: boolean): void {
    const target = scrollTarget();
    const x = target.scrollLeft;
    const y = 0;
    new Scroller(target, smooth).scrollTo(x, y);
  }

  scrollToBottom(smooth: boolean): void {
    const target = scrollTarget();
    const x = target.scrollLeft;
    const y = target.scrollHeight;
    new Scroller(target, smooth).scrollTo(x, y);
  }

  scrollToHome(smooth: boolean): void {
    const target = scrollTarget();
    const x = 0;
    const y = target.scrollTop;
    new Scroller(target, smooth).scrollTo(x, y);
  }

  scrollToEnd(smooth: boolean): void {
    const target = scrollTarget();
    const x = target.scrollWidth;
    const y = target.scrollTop;
    new Scroller(target, smooth).scrollTo(x, y);
  }
}