/*
 * https://github.com/morethanwords/tweb
 * Copyright (C) 2019-2021 Eduard Kuzmenko
 * https://github.com/morethanwords/tweb/blob/master/LICENSE
 */

// * Jolly Cobra's fastSmoothScroll slightly patched

import {dispatchHeavyAnimationEvent} from '../hooks/useHeavyAnimationCheck';
import {fastRafPromise} from './schedulers';
import {animateSingle, cancelAnimationByKey} from './animation';
import rootScope from '../lib/rootScope';
import isInDOM from './dom/isInDOM';
import liteMode from './liteMode';

const MIN_JS_DURATION = 250;
const MAX_JS_DURATION = 600;
const LONG_TRANSITION_MAX_DISTANCE = 1500;
const SHORT_TRANSITION_MAX_DISTANCE = 500;

export enum FocusDirection {
  Up,
  Down,
  Static,
};

export type ScrollGetNormalSizeCallback = (options: {rect: DOMRect}) => number;
export type ScrollGetElementPositionCallback = (options: {elementRect: DOMRect, containerRect: DOMRect, elementPosition: number}) => number;
export type ScrollStartCallbackDimensions = {
  scrollSize: number,
  scrollPosition: number,
  distanceToEnd: number,
  path: number,
  duration: number,
  containerRect: DOMRect,
  elementRect: DOMRect,
  getProgress: () => number
};

export type ScrollOptions = {
  container: HTMLElement,
  element: HTMLElement,
  position: ScrollLogicalPosition,
  margin?: number,
  maxDistance?: number,
  forceDirection?: FocusDirection,
  forceDuration?: number,
  axis?: 'x' | 'y',
  getNormalSize?: ScrollGetNormalSizeCallback,
  getElementPosition?: ScrollGetElementPositionCallback,
  fallbackToElementStartWhenCentering?: HTMLElement,
  startCallback?: (dimensions: ScrollStartCallbackDimensions) => void,
  transitionFunction?: (value: number) => number
};

export default function fastSmoothScroll(options: ScrollOptions) {
  options.margin ??= 0;
  options.maxDistance ??= LONG_TRANSITION_MAX_DISTANCE;
  options.axis ??= 'y';
  // return;

  if(!liteMode.isAvailable('animations') || options.forceDuration === 0) {
    options.forceDirection = FocusDirection.Static;
  }

  if(options.forceDirection === FocusDirection.Static) {
    options.forceDuration = 0;
    return scrollWithJs(options);
    /* return Promise.resolve();

    element.scrollIntoView({ block: position });

    cancelAnimationByKey(container);
    return Promise.resolve(); */
  }

  const promise = fastRafPromise().then(() => scrollWithJs(options));

  return options.axis === 'y' ? dispatchHeavyAnimationEvent(promise) : promise;
}

function scrollWithJs(options: ScrollOptions): Promise<void> {
  const {element, container, getNormalSize, getElementPosition, transitionFunction, axis, margin, position, forceDirection, maxDistance, forceDuration} = options;
  if(!isInDOM(element)) {
    cancelAnimationByKey(container);
    return Promise.resolve();
  }

  const rectStartKey = axis === 'y' ? 'top' : 'left';
  const rectEndKey = axis === 'y' ? 'bottom' : 'right';
  const sizeKey = axis === 'y' ? 'height' : 'width';
  const scrollSizeKey = axis === 'y' ? 'scrollHeight' : 'scrollWidth';
  const elementScrollSizeKey = axis === 'y' ? 'scrollHeight' : 'offsetWidth'; // can use offsetWidth for X, since it's almost same as scrollWidth
  const scrollPositionKey = axis === 'y' ? 'scrollTop' : 'scrollLeft';

  // const { offsetTop: elementTop, offsetHeight: elementHeight } = element;
  const elementRect = element.getBoundingClientRect();
  const containerRect = container.getBoundingClientRect ? container.getBoundingClientRect() : document.body.getBoundingClientRect();

  // const transformable = container.firstElementChild as HTMLElement;

  const possibleElementPosition = elementRect[rectStartKey] - containerRect[rectStartKey];
  const elementPosition = getElementPosition ? getElementPosition({elementRect, containerRect, elementPosition: possibleElementPosition}) : possibleElementPosition;
  const elementSize = element[elementScrollSizeKey]; // margin is exclusive in DOMRect

  const containerSize = getNormalSize ? getNormalSize({rect: containerRect}) : containerRect[sizeKey];

  let scrollPosition = container[scrollPositionKey];
  const scrollSize = container[scrollSizeKey];
  /* const elementPosition = element.offsetTop;
  const elementSize = element.offsetHeight;

  const scrollPosition = container[scrollPositionKey];
  const scrollSize = container[scrollSizeKey];
  const containerSize = container.offsetHeight; */

  let path!: number;

  switch(position) {
    case 'start':
      path = elementPosition - margin;
      break;
    case 'end':
      path = elementRect[rectEndKey] /* + (elementSize - elementRect[sizeKey]) */ - containerRect[rectEndKey] + margin;
      break;
    // 'nearest' is not supported yet
    case 'nearest':
    case 'center':
      if(elementSize < containerSize) {
        path = (elementPosition + elementSize / 2) - (containerSize / 2);
      } else {
        if(options.fallbackToElementStartWhenCentering && options.fallbackToElementStartWhenCentering !== element) {
          options.element = options.fallbackToElementStartWhenCentering;
          options.position = 'start';
          return scrollWithJs(options);
        }

        path = elementPosition - margin;
      }

      break;
  }
  /* switch (position) {
    case 'start':
      path = (elementPosition - margin) - scrollPosition;
      break;
    case 'end':
      path = (elementPosition + elementSize + margin) - (scrollPosition + containerSize);
      break;
    // 'nearest' is not supported yet
    case 'nearest':
    case 'center':
      path = elementSize < containerSize
        ? (elementPosition + elementSize / 2) - (scrollPosition + containerSize / 2)
        : (elementPosition - margin) - scrollPosition;
      break;
  } */

  if(Math.abs(path - (margin || 0)) < 1) {
    cancelAnimationByKey(container);
    return Promise.resolve();
  }

  if(axis === 'y') {
    if(forceDirection === undefined) {
      if(path > maxDistance) {
        scrollPosition = container.scrollTop += path - maxDistance;
        path = maxDistance;
      } else if(path < -maxDistance) {
        scrollPosition = container.scrollTop += path + maxDistance;
        path = -maxDistance;
      }
    }/*  else if(forceDirection === FocusDirection.Up) { // * not tested yet
      container.scrollTop = offsetTop + container.scrollTop + maxDistance;
    } else if(forceDirection === FocusDirection.Down) { // * not tested yet
      container.scrollTop = Math.max(0, offsetTop + container.scrollTop - maxDistance);
    } */
  }

  // console.log('scrollWithJs: will scroll path:', path, element);

  /* let existsTransform = 0;
  const currentTransform = transformable.style.transform;
  if(currentTransform) {
    existsTransform = parseInt(currentTransform.match(/\((.+?), (.+?), .+\)/)[2]);
    //path += existsTransform;
  } */

  if(path < 0) {
    const remainingPath = -scrollPosition;
    path = Math.max(path, remainingPath);
  } else if(path > 0) {
    const remainingPath = scrollSize - (scrollPosition + containerSize);
    path = Math.min(path, remainingPath);
  }

  const target = container[scrollPositionKey] + path;
  const absPath = Math.abs(path);
  const duration = forceDuration ?? (
    MIN_JS_DURATION + (absPath / LONG_TRANSITION_MAX_DISTANCE) * (MAX_JS_DURATION - MIN_JS_DURATION)
  );
  const startAt = Date.now();

  /* transformable.classList.add('no-transition');

  const tickTransform = () => {
    const t = duration ? Math.min((Date.now() - startAt) / duration, 1) : 1;
    const currentPath = path * transition(t);

    transformable.style.transform = `translate3d(0, ${-currentPath}px, 0)`;
    container.dataset.translate = '' + -currentPath;

    const willContinue = t < 1;
    if(!willContinue) {
      fastRaf(() => {
        delete container.dataset.transform;
        container.dataset.transform = '';
        transformable.style.transform = '';
        void transformable.offsetLeft; // reflow
        transformable.classList.remove('no-transition');
        void transformable.offsetLeft; // reflow
        container[scrollPositionKey] = Math.round(target);
      });
    }

    return willContinue;
  };

  return animateSingle(tickTransform, container); */

  /* return new Promise((resolve) => {
    fastRaf(() => {
      transformable.style.transform = '';
      transformable.style.transition = '';

      setTimeout(resolve, duration);
    });
  });

  const transformableHeight = transformable.scrollHeight;
  //transformable.style.minHeight = `${transformableHeight}px`;
  */

  const transition = transitionFunction ?? (absPath < SHORT_TRANSITION_MAX_DISTANCE ? shortTransition : longTransition);
  const getProgress = () => duration ? Math.min((Date.now() - startAt) / duration, 1) : 1;
  const tick = () => {
    const t = getProgress();
    const value = transition(t);
    const currentPath = path * (1 - value);
    container[scrollPositionKey] = Math.round(target - currentPath);

    return t < 1;
  };

  if(!duration || !path) {
    cancelAnimationByKey(container);
    tick();
    return Promise.resolve();
  }

  /* return new Promise((resolve) => {
    setTimeout(resolve, duration);
  }).then(() => {
    transformable.classList.add('no-transition');
    void transformable.offsetLeft; // reflow
    transformable.style.transform = '';
    transformable.style.transition = '';
    void transformable.offsetLeft; // reflow
    transformable.classList.remove('no-transition');
    void transformable.offsetLeft; // reflow
    fastRaf(() => {

      container[scrollPositionKey] = Math.round(target);
      //transformable.style.minHeight = ``;
    });

  }); */

  if(options.startCallback) {
    const distanceToEnd = scrollSize - Math.round(target + container[axis === 'y' ? 'offsetHeight' : 'offsetWidth']);
    options.startCallback({
      scrollSize,
      scrollPosition,
      distanceToEnd,
      path,
      duration,
      containerRect,
      elementRect,
      getProgress
    });
  }

  return animateSingle(tick, container);
}

function longTransition(t: number) {
  return 1 - ((1 - t) ** 5);
}

function shortTransition(t: number) {
  return 1 - ((1 - t) ** 3.5);
}
