// SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

import debounce from 'lodash.debounce';

export const MOVEMENT_INDICATOR_ELEMENT_CLASS = 'movement-indicator';
const MOVEMENT_DEBOUNCE = 250;
const MOVEMENT_DEADZONE = 5;

export class MovementObserver {

  element;
  trailingCallback = () => {};
  leadingCallback = () => {};

  _intersectionObserver;
  _debouncedIntersectChange;
  _debouncedLeadingCallback;
  _oldPosition;

  constructor(element, options) {
    this.element = element;

    if (!this.element) {
      throw new Error('Cannot setup MovementObserver without element');
    }

    if (options && options.trailingCallback) {
      this.trailingCallback = options.trailingCallback;
      if (typeof this.trailingCallback !== 'function') {
	throw new Error('Cannot setup MovementObserver with trailingCallback not being a function');
      }
    }

    if (options && options.leadingCallback) {
      this.leadingCallback = options.leadingCallback;
      if (typeof this.leadingCallback !== 'function') {
	throw new Error('Cannot setup MovementObserver with leadingCallback not being a function');
      }
    }

    this._debouncedIntersectChange = debounce(this._intersectChange.bind(this), MOVEMENT_DEBOUNCE, { leading: false, trailing: true });
    this._debouncedLeadingCallback = debounce(() => { this._positionUpdate(false, true); }, MOVEMENT_DEBOUNCE, { leading: true, trailing: false });
  }

  observe() {
    this._resetIntersectionObserver();
    this._positionUpdate(false, false);
    this._intersectionObserver.observe(this.element);
  }

  unobserve() {
    // console.log('MovementObserver', 'unobserve');
    this._debouncedIntersectChange.cancel();
    this._debouncedLeadingCallback.cancel();
    if (this._intersectionObserver) {
      this._intersectionObserver.unobserve(this.element);
      this._intersectionObserver.disconnect();
    }
    this._intersectionObserver = undefined;
    this._oldPosition = undefined;
  }

  _intersectChange(entries, observer) {
    // console.log('MovementObserver', '_intersectChange', entries, observer);

    if (!this._positionUpdate(true, false))
      return;

    observer.disconnect();
    observer.unobserve(this.element);
    this.observe();
  }

  _positionUpdate(trailingCallback, leadingCallback) {
    const currentPosition = this.element.getBoundingClientRect();

    if (!this._oldPosition || Math.abs(this._oldPosition.top - currentPosition.top) > MOVEMENT_DEADZONE || Math.abs(this._oldPosition.left - currentPosition.left) > MOVEMENT_DEADZONE) {
      // console.log('MovementObserver', '_positionUpdate', leadingCallback, trailingCallback);
      
      if (leadingCallback)
	this.leadingCallback();

      if (trailingCallback)
	this.trailingCallback();

      if (!leadingCallback)
	this._oldPosition = currentPosition;

      return true;
    }

    return false;
  }

  _resetIntersectionObserver() {
    // console.log('MovementObserver', '_resetIntersectionObserver');

    const currentPosition = this.element.getBoundingClientRect();
    const windowWidth = window.innerWidth || document.documentElement.clientWidth;
    const windowHeight = window.innerHeight || document.documentElement.clientHeight;

    const top = -1 * (currentPosition.top - MOVEMENT_DEADZONE);
    const left = -1 * (currentPosition.left - MOVEMENT_DEADZONE);
    const bottom = -1 * (windowHeight - currentPosition.bottom - MOVEMENT_DEADZONE);
    const right = -1 * (windowWidth - currentPosition.right - MOVEMENT_DEADZONE);
    
    const observerOptions = {
      rootMargin: `${top}px ${right}px ${bottom}px ${left}px`,
      threshold: 0.9,
    };

    const observerCallback = (event, observer) => {
      this._debouncedLeadingCallback();
      this._debouncedIntersectChange(event, observer);
    };
    
    if (this._intersectionObserver) {
      this._debouncedIntersectChange.cancel();
      this._debouncedLeadingCallback.cancel();
      this._intersectionObserver.unobserve(this.element);
      this._intersectionObserver.disconnect();
      this._intersectionObserver = undefined;
    }
    this._intersectionObserver = new IntersectionObserver(observerCallback.bind(this), observerOptions);
  }
}
