/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @cspell/spellchecker */
// import './main.scss';
import { JSX } from 'react/jsx-runtime';
import { DIRECTION } from './types';
import { isTreeScrollable } from './is-scrollable';
import { RefreshingContent } from './refreshing-content';
import React, { 
  useEffect, 
  useRef 
} from 'react';

const MAX = 128;
const k = 0.4;
function appr(x: number) {
  return MAX * (1 - Math.exp((-k * x) / MAX));
}

interface PullToRefreshProps {
  isPullable?: boolean;
  canFetchMore?: boolean;
  onRefresh: () => Promise<any>;
  onFetchMore?: () => Promise<any>;
  refreshingContent?: JSX.Element | string;
  children: JSX.Element;
  pullDownThreshold?: number;
  fetchMoreThreshold?: number;
  maxPullDownDistance?: number;
  backgroundColor?: string;
  className?: string;
}

export const PullToRefresh: React.FC<PullToRefreshProps> = ({
  isPullable = true,
  canFetchMore = false,
  onRefresh,
  onFetchMore,
  refreshingContent = <RefreshingContent />,
  children,
  pullDownThreshold = 400,
  fetchMoreThreshold = 100,
  maxPullDownDistance = 99999, // max distance to scroll to trigger refresh
  backgroundColor,
  className = '',
}) => {
  const containerRef = useRef<HTMLDivElement>(null);
  const childrenRef = useRef<HTMLDivElement>(null);
  const pullDownRef = useRef<HTMLDivElement>(null);
  const fetchMoreRef = useRef<HTMLDivElement>(null);
  let pullToRefreshThresholdBreached = false;
  let fetchMoreTresholdBreached = false; // if true, fetchMore loader is displayed
  let isDragging = false;
  let startY = 0;
  let currentY = 0;

  useEffect(() => {
    if(!isPullable || !childrenRef?.current){
      return () => {};
    }
    const childrenEl = childrenRef.current;
    childrenEl.addEventListener('touchstart', onTouchStart, { passive: true });
    childrenEl.addEventListener('mousedown', onTouchStart);
    childrenEl.addEventListener('touchmove', onTouchMove, { passive: false });
    childrenEl.addEventListener('mousemove', onTouchMove);
    childrenEl.addEventListener('touchend', onEnd);
    childrenEl.addEventListener('mouseup', onEnd);
    document.body.addEventListener('mouseleave', onEnd);
    return () => {
      childrenEl.removeEventListener('touchstart', onTouchStart);
      childrenEl.removeEventListener('mousedown', onTouchStart);
      childrenEl.removeEventListener('touchmove', onTouchMove);
      childrenEl.removeEventListener('mousemove', onTouchMove);
      childrenEl.removeEventListener('touchend', onEnd);
      childrenEl.removeEventListener('mouseup', onEnd);
      document.body.removeEventListener('mouseleave', onEnd);
    };
  }, [children, isPullable, onRefresh, pullDownThreshold, maxPullDownDistance, canFetchMore, fetchMoreThreshold]);

  /**
   * Check onMount / canFetchMore becomes true
   *  if fetchMoreThreshold is already breached
   */
  useEffect(() => {
    /**
     * Check if it is already in fetching more state
     */
    if (!containerRef?.current) return;
    const isAlreadyFetchingMore = containerRef.current.classList.contains('ptr--fetch-more-treshold-breached');
    if (isAlreadyFetchingMore) return;
    /**
     * Proceed
     */
    if (canFetchMore && getScrollToBottomValue() < fetchMoreThreshold && onFetchMore) {
      containerRef.current.classList.add('ptr--fetch-more-treshold-breached');
      fetchMoreTresholdBreached = true;
      onFetchMore().then(initContainer).catch(initContainer);
    }
  }, [canFetchMore, children]);

  useEffect(() => {
    if (childrenRef.current) {
      childrenRef.current.style.height = isPullable ? '100%' : 'auto';
    }
    if (pullDownRef.current) {
      pullDownRef.current.style.opacity = isPullable ? '1' : '0';
    }
    if (containerRef.current) {
      containerRef.current.style.height = isPullable ? '100%' : 'auto';
    }
  }, [isPullable]);

  /**
   * Returns distance to bottom of the container
   */
  const getScrollToBottomValue = (): number => {
    if (!childrenRef?.current) return -1;
    const scrollTop = window.scrollY; // is the pixels hidden in top due to the scroll. With no scroll its value is 0.
    const { scrollHeight } = childrenRef.current; // is the pixels of the whole container
    return scrollHeight - scrollTop - window.innerHeight;
  };

  const initContainer = (): void => {
    requestAnimationFrame(() => {
      /**
       * Reset Styles
       */

      if (childrenRef.current) {
        childrenRef.current.style.overflowX = 'hidden';
        childrenRef.current.style.overflowY = 'auto';
        childrenRef.current.style.transform = `unset`;
        childrenRef.current.style.height = isPullable ? '100%' : 'auto';
      }
      if (pullDownRef.current) {
        pullDownRef.current.style.opacity = isPullable ? '1' : '0';
      }
      if (containerRef.current) {
        containerRef.current.style.height = isPullable ? '100%' : 'auto';
        containerRef.current.classList.remove('ptr--pull-down-treshold-breached');
        containerRef.current.classList.remove('ptr--dragging');
        containerRef.current.classList.remove('ptr--fetch-more-treshold-breached');
      }

      if (pullToRefreshThresholdBreached) pullToRefreshThresholdBreached = false;
      if (fetchMoreTresholdBreached) fetchMoreTresholdBreached = false;
    });
  };

  const onTouchStart = (e: MouseEvent | TouchEvent): void => {
    isDragging = false;
    if (e instanceof MouseEvent) {
      startY = e.pageY;
    }

    if (window.TouchEvent && e instanceof TouchEvent) {
      startY = e.touches?.[0]?.pageY ?? 0;
    }

    currentY = startY;

    // Check if element can be scrolled
    if (e.type === 'touchstart' && isTreeScrollable(e.target as HTMLElement, DIRECTION.UP)) {
      return;
    }
    // Top non visible so cancel
    if (childrenRef.current!.getBoundingClientRect().top < 0) {
      return;
    }
    isDragging = true;
  };

  const onTouchMove = (e: MouseEvent | TouchEvent): void => {
    if (!isDragging) {
      return;
    }

    if (window.TouchEvent && e instanceof TouchEvent) {
      currentY = e.touches?.[0]?.pageY ?? 0;
    } else {
      currentY = (e as MouseEvent).pageY;
    }

    containerRef.current!.classList.add('ptr--dragging');

    if (currentY < startY) {
      isDragging = false;
      return;
    }

    if (e.cancelable) {
      e.preventDefault();
    }

    const yDistanceMoved = Math.min(currentY - startY, maxPullDownDistance);

    // Limit to trigger refresh has been breached
    if (yDistanceMoved >= pullDownThreshold) {
      isDragging = true;
      pullToRefreshThresholdBreached = true;
      containerRef.current!.classList.remove('ptr--dragging');
      containerRef.current!.classList.add('ptr--pull-down-treshold-breached');
    }

    // maxPullDownDistance breached, stop the animation
    if (yDistanceMoved >= maxPullDownDistance) {
      return;
    }

    // pullDownRef.current!.style.opacity = (yDistanceMoved / 65).toString();
    childrenRef.current!.style.overflow = 'visible';
    childrenRef.current!.style.transform = `translate(0px, ${appr(yDistanceMoved)}px)`;
    pullDownRef.current!.style.visibility = 'visible';
  };

  const onEnd = (): void => {
    isDragging = false;
    startY = 0;
    currentY = 0;

    // Container has not been dragged enough, put it back to it's initial state
    if (!pullToRefreshThresholdBreached) {
      if (pullDownRef.current) pullDownRef.current.style.visibility = 'visible';
      initContainer();
      return;
    }

    if (childrenRef.current) {
      childrenRef.current.style.overflow = 'visible';
      childrenRef.current.style.transform = `translate(0px, ${pullDownThreshold}px)`;
    }

    onRefresh().then(initContainer).catch(initContainer);
  };

  return (
    <div className={`ptr ${className}`} style={{ backgroundColor }} ref={containerRef}>
      {/*
      <div className="ptr__pull-down" ref={pullDownRef}>
        <div className="ptr__loader ptr__pull-down--loading">{refreshingContent}</div>
      </div>
      */}
      <div className="ptr__children" ref={childrenRef}>
        {children}
        {/*
        <div className="ptr__fetch-more" ref={fetchMoreRef}>
          <div className="ptr__loader ptr__fetch-more--loading">{refreshingContent}</div>
        </div>
        */}
      </div>
    </div>
  );
};
