import { ScrollableWindow } from './scrollableWindow';
import { ScrollPosition } from './scrollPosition';
import { DebouncedFunc } from 'lodash';
import { EventEmitter } from '@wix/bex-core/events';
import { PartialLodash } from '@wix/bex-core';

const debounceOptions = { trailing: true };
const debounceDelay = 400;
const scrollIdleDelay = 250;

export interface ReachBottomListenerParams {
  // distance from the bottom to trigger the callback
  offsetPX?: number;
  onReachBottom: () => unknown;
  // throttle scroll event, debounce resize event
  delay?: number;
  container: ScrollableWindow;
  target?: HTMLElement | null;
  lodash: PartialLodash;
}

export interface ReachBottomListener extends ReachBottomListenerParams {}
export class ReachBottomListener {
  scrollPosition!: ScrollPosition;
  offsetPX: number;
  onReachBottom: () => unknown;

  scrollListener: () => Promise<void> | undefined;
  container: ScrollableWindow;
  _target?: HTMLElement | null;
  targetScrollingParent?: HTMLElement | null;
  triggerListenerIfReached: DebouncedFunc<() => Promise<boolean>>;
  isScrolling = false;

  events = new EventEmitter();

  debounceSetScrollingIdle: () => unknown;

  constructor(params: ReachBottomListenerParams) {
    this.container = params.container;
    this.target = params.target;
    this.offsetPX = params.offsetPX ?? 0;
    this.onReachBottom = params.onReachBottom;
    const delay = Math.max(params.delay || 0, 0);

    const {
      lodash: { throttle, debounce },
    } = params;
    this.scrollListener = delay
      ? throttle(this.handleScroll, delay)
      : this.handleScroll;

    this.debounceSetScrollingIdle = debounce(() => {
      this.isScrolling = false;
      this.events.emit('idle');
    }, Math.max(scrollIdleDelay, delay));

    this.triggerListenerIfReached = debounce(
      async () => {
        this.scrollPosition = this.currentScrollPosition();

        const { onReachBottom, scrollPosition, bottomX, offsetPX } = this;

        if (
          onReachBottom != null &&
          scrollPosition.reachedTo(bottomX, offsetPX)
        ) {
          await onReachBottom();
          return true;
        }

        return false;
      },
      debounceDelay,
      debounceOptions,
    );
  }

  set target(value: HTMLElement | null | undefined) {
    this._target = value;
    this.targetScrollingParent = null;

    if (this._target != null) {
      let offsetParent = this._target.offsetParent;
      while (
        offsetParent instanceof HTMLElement &&
        offsetParent?.offsetParent !== this.container
      ) {
        offsetParent = offsetParent.offsetParent;
      }
      if (offsetParent instanceof HTMLElement) {
        this.targetScrollingParent = offsetParent;
      }
    }
  }

  get target() {
    return this._target;
  }

  currentScrollPosition() {
    const { scrollHeight, clientHeight, scrollTop } = this.container;
    return new ScrollPosition({ clientHeight, scrollHeight, scrollTop });
  }

  handleScroll = async () => {
    const { clientHeight, scrollHeight, scrollTop } = this.container;
    this.isScrolling = true;
    this.debounceSetScrollingIdle();
    const newScrollPosition = new ScrollPosition({
      clientHeight,
      scrollHeight,
      scrollTop,
    });

    const { offsetPX, onReachBottom, scrollPosition, bottomX } = this;

    if (
      onReachBottom != null &&
      newScrollPosition.reachingTo(scrollPosition, bottomX, offsetPX)
    ) {
      await onReachBottom();
    }
    this.scrollPosition = newScrollPosition;
  };

  get bottomX() {
    // either the container bottom, or the target element offset from the container
    const { target, targetScrollingParent, scrollPosition } = this;
    if (target != null && targetScrollingParent != null) {
      return target.offsetTop + targetScrollingParent.offsetTop;
    }
    return scrollPosition.scrollHeight;
  }

  resizeListener = () => {
    this.triggerListenerIfReached();
  };

  subscribe() {
    this.scrollPosition = this.currentScrollPosition();
    this.container.addEventListener('scroll', this.scrollListener);
    window.addEventListener('resize', this.resizeListener);
    return this;
  }

  unsubscribe() {
    this.container.removeEventListener('scroll', this.scrollListener);
    window.removeEventListener('resize', this.resizeListener);
  }
}
