import { useEffect, useRef, RefObject, useCallback } from 'react';

const debug: boolean = false;

type ResizeCallback = () => void;

// Define the debounce function with cancel capability
function debounce<T extends (...args: any[]) => void>(func: T, wait: number) {
  let timeout: NodeJS.Timeout | null = null;
  let lastCall: number = 0;

  const debounced = function (...args: Parameters<T>) {
    const now = Date.now();

    // Execute immediately if wait time has passed since last call
    if (now - lastCall >= wait) {
      lastCall = now;
      // Clear any existing timeout to prevent stale executions
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      func(...args);
      return;
    }

    // Schedule for remaining time if no timeout is pending
    if (!timeout) {
      timeout = setTimeout(() => {
        lastCall = Date.now();
        func(...args);
        timeout = null;
      }, wait - (now - lastCall));
    }
  };

  // Add cancel method to clear pending timeout
  debounced.cancel = function () {
    if (timeout) {
      clearTimeout(timeout);
      timeout = null;
    }
  };

  return debounced;
}

const useResizeObserverAndMutationObserver = (
  targetRef: RefObject<HTMLElement | null>,
  scrollToRef: RefObject<HTMLElement | null> | null,
  callback: ResizeCallback
) => {
  const callbackRef = useRef(callback);
  const resizeObserverRef = useRef<ResizeObserver | null>(null);
  const mutationObserverRef = useRef<MutationObserver | null>(null);

  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  useEffect(() => {
    const container = targetRef.current;
    const scrollTo = scrollToRef?.current;
    if (!container) return;

    const debouncedCallback = debounce((target: string) => {
      debug && console.debug(`"debouncedCallback(${target})`);
      requestAnimationFrame(() => callbackRef.current());
    }, 500);

    const resizeObserver = new ResizeObserver((e: any) => { debouncedCallback("resize"); });
    const mutationObserver = new MutationObserver((e: any) => { debouncedCallback("mutation"); });

    // Observe container size
    resizeObserver.observe(container);
    // Observe TextField size if available
    if (scrollTo) {
      resizeObserver.observe(scrollTo);
    }

    // Observe child additions/removals and text content changes
    mutationObserver.observe(container, {
      childList: true,
      subtree: true,
      characterData: true,
    });

    resizeObserverRef.current = resizeObserver;
    mutationObserverRef.current = mutationObserver;

    return () => {
      debouncedCallback.cancel();
      resizeObserver.disconnect();
      mutationObserver.disconnect();
    };
  }, [targetRef, scrollToRef]);
};

/**
 * Auto-scroll hook for scrollable containers.
 * Scrolls to the bottom of the container on paste or when TextField is fully/partially visible.
 */
const useAutoScrollToBottom = (
  scrollToRef: RefObject<HTMLElement | null>,
  smooth: boolean = true,
  fallbackThreshold: number = 0.33,
  contentUpdateTrigger?: any
): RefObject<HTMLDivElement | null> => {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const lastScrollTop = useRef(0);
  const scrollTimeout = useRef<NodeJS.Timeout | null>(null);
  const isUserScrollingUpRef = useRef(false);

  const checkAndScrollToBottom = useCallback((isPasteEvent: boolean = false) => {
    const container = containerRef.current;
    if (!container) return;

    let shouldScroll = false;
    const scrollTo = scrollToRef.current;

    if (isPasteEvent && !scrollTo) {
      console.error("Paste Event triggered without scrollTo");
    }

    if (scrollTo) {
      // Get positions
      const containerRect = container.getBoundingClientRect();
      const scrollToRect = scrollTo.getBoundingClientRect();
      const containerTop = containerRect.top;
      const containerBottom = containerTop + container.clientHeight;

      // Check if TextField is fully or partially visible (for non-paste events)
      const isTextFieldVisible =
        scrollToRect.top < containerBottom && scrollToRect.bottom > containerTop;

      // Scroll on paste or if TextField is visible and user isn't scrolling up
      shouldScroll = isPasteEvent || (isTextFieldVisible && !isUserScrollingUpRef.current);
      if (shouldScroll) {
        requestAnimationFrame(() => {
          debug && console.debug('Scrolling to container bottom:', {
            scrollHeight: container.scrollHeight,
            scrollToHeight: scrollToRect.height,
            containerHeight: container.clientHeight,
            isPasteEvent,
            isTextFieldVisible,
            isUserScrollingUp: isUserScrollingUpRef.current,
          });
          container.scrollTo({
            top: container.scrollHeight,
            behavior: smooth ? 'smooth' : 'auto',
          });
        });
      }
    } else {
      // Fallback to threshold-based check
      const scrollHeight = container.scrollHeight;
      const isNearBottom =
        scrollHeight - container.scrollTop - container.clientHeight <=
        container.clientHeight * fallbackThreshold;
      shouldScroll = isNearBottom && !isUserScrollingUpRef.current;

      if (shouldScroll) {
        requestAnimationFrame(() => {
          debug && console.debug('Scrolling to container bottom (fallback):', { scrollHeight });
          container.scrollTo({
            top: container.scrollHeight,
            behavior: smooth ? 'smooth' : 'auto',
          });
        });
      }
    }
  }, [fallbackThreshold, smooth, scrollToRef]);

  useEffect(() => {
    const container = containerRef.current;
    const scrollTo = scrollToRef.current;
    if (!container) return;

    const handleScroll = (ev: Event, pause?: number) => {
      const currentScrollTop = container.scrollTop;
      /* If the user is scrolling up *or* they used the scroll wheel and didn't scroll, 
       * they may be zooming in a region; pause scrolling */
      isUserScrollingUpRef.current = (currentScrollTop <= lastScrollTop.current) || pause ? true : false;
      debug && console.debug(`Scrolling up or paused: ${isUserScrollingUpRef.current} ${pause}`);
      lastScrollTop.current = currentScrollTop;

      if (scrollTimeout.current) clearTimeout(scrollTimeout.current);
      scrollTimeout.current = setTimeout(() => {
        isUserScrollingUpRef.current = false;
        debug && console.debug(`Scrolling up: ${isUserScrollingUpRef.current}`);
      }, pause ? pause : 500);
    };

    const pauseScroll = (ev: Event) => {
      debug && console.log("Pausing for mouse movement");
      handleScroll(ev, 500);
    }

    const pauseClick = (ev: Event) => {
      debug && console.log("Pausing for mouse click");
      handleScroll(ev, 1000);
    }

    const handlePaste = () => {
      console.log("handlePaste");
      // Delay scroll check to ensure DOM updates
      setTimeout(() => {
        console.log("scrolling for handlePaste");
        requestAnimationFrame(() => checkAndScrollToBottom(true));
      }, 100);
    };

    window.addEventListener('mousemove', pauseScroll);
    window.addEventListener('mousedown', pauseClick);

    container.addEventListener('scroll', handleScroll);
    if (scrollTo) {
      scrollTo.addEventListener('paste', handlePaste);
    }
    checkAndScrollToBottom();

    return () => {
      window.removeEventListener('mousedown', pauseClick);
      window.removeEventListener('mousemove', pauseScroll);
      container.removeEventListener('scroll', handleScroll);
      if (scrollTo) {
        scrollTo.removeEventListener('paste', handlePaste);
      }
      if (scrollTimeout.current) clearTimeout(scrollTimeout.current);
    };
  }, [smooth, scrollToRef, fallbackThreshold, contentUpdateTrigger, checkAndScrollToBottom]);

  // Observe container and TextField size, plus DOM changes
  useResizeObserverAndMutationObserver(containerRef, scrollToRef, checkAndScrollToBottom);

  return containerRef;
};

export {
  useResizeObserverAndMutationObserver,
  useAutoScrollToBottom
}