/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {
  useRef,
  forwardRef,
  useImperativeHandle,
  useCallback,
  useMemo,
  useLayoutEffect,
} from 'react';
import type React from 'react';
import {
  VirtualizedList,
  type VirtualizedListRef,
  type VirtualizedListProps,
  SCROLL_TO_ITEM_END,
} from './VirtualizedList.js';
import { useScrollable } from '../../contexts/ScrollProvider.js';
import { Box, type DOMElement } from 'ink';
import { useAnimatedScrollbar } from '../../hooks/useAnimatedScrollbar.js';
import { useKeypress, type Key } from '../../hooks/useKeypress.js';
import { Command } from '../../key/keyMatchers.js';
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';

const ANIMATION_FRAME_DURATION_MS = 33;

interface ScrollableListProps<T> extends VirtualizedListProps<T> {
  hasFocus: boolean;
  width?: string | number;
  scrollbar?: boolean;
  stableScrollback?: boolean;
  copyModeEnabled?: boolean;
  isStatic?: boolean;
  fixedItemHeight?: boolean;
  targetScrollIndex?: number;
  containerHeight?: number;
  scrollbarThumbColor?: string;
}

export type ScrollableListRef<T> = VirtualizedListRef<T>;

function ScrollableList<T>(
  props: ScrollableListProps<T>,
  ref: React.Ref<ScrollableListRef<T>>,
) {
  const keyMatchers = useKeyMatchers();
  const { hasFocus, width, scrollbar = true, stableScrollback } = props;
  const virtualizedListRef = useRef<VirtualizedListRef<T>>(null);
  const containerRef = useRef<DOMElement>(null);

  useImperativeHandle(
    ref,
    () => ({
      scrollBy: (delta) => virtualizedListRef.current?.scrollBy(delta),
      scrollTo: (offset) => virtualizedListRef.current?.scrollTo(offset),
      scrollToEnd: () => virtualizedListRef.current?.scrollToEnd(),
      scrollToIndex: (params) =>
        virtualizedListRef.current?.scrollToIndex(params),
      scrollToItem: (params) =>
        virtualizedListRef.current?.scrollToItem(params),
      getScrollIndex: () => virtualizedListRef.current?.getScrollIndex() ?? 0,
      getScrollState: () =>
        virtualizedListRef.current?.getScrollState() ?? {
          scrollTop: 0,
          scrollHeight: 0,
          innerHeight: 0,
        },
    }),
    [],
  );

  const getScrollState = useCallback(
    () =>
      virtualizedListRef.current?.getScrollState() ?? {
        scrollTop: 0,
        scrollHeight: 0,
        innerHeight: 0,
      },
    [],
  );

  const scrollBy = useCallback((delta: number) => {
    virtualizedListRef.current?.scrollBy(delta);
  }, []);

  const { scrollbarColor, flashScrollbar, scrollByWithAnimation } =
    useAnimatedScrollbar(hasFocus, scrollBy);

  const smoothScrollState = useRef<{
    active: boolean;
    start: number;
    from: number;
    to: number;
    duration: number;
    timer: NodeJS.Timeout | null;
  }>({ active: false, start: 0, from: 0, to: 0, duration: 0, timer: null });

  const stopSmoothScroll = useCallback(() => {
    if (smoothScrollState.current.timer) {
      clearInterval(smoothScrollState.current.timer);
      smoothScrollState.current.timer = null;
    }
    smoothScrollState.current.active = false;
  }, []);

  useLayoutEffect(() => stopSmoothScroll, [stopSmoothScroll]);

  const smoothScrollTo = useCallback(
    (
      targetScrollTop: number,
      duration: number = process.env['NODE_ENV'] === 'test' ? 0 : 200,
    ) => {
      stopSmoothScroll();

      const scrollState = virtualizedListRef.current?.getScrollState() ?? {
        scrollTop: 0,
        scrollHeight: 0,
        innerHeight: 0,
      };
      const {
        scrollTop: rawStartScrollTop,
        scrollHeight,
        innerHeight,
      } = scrollState;

      const maxScrollTop = Math.max(0, scrollHeight - innerHeight);
      const startScrollTop = Math.min(rawStartScrollTop, maxScrollTop);

      let effectiveTarget = targetScrollTop;
      if (
        targetScrollTop === SCROLL_TO_ITEM_END ||
        targetScrollTop >= maxScrollTop
      ) {
        effectiveTarget = maxScrollTop;
      }

      const clampedTarget = Math.max(
        0,
        Math.min(maxScrollTop, effectiveTarget),
      );

      if (duration === 0) {
        if (
          targetScrollTop === SCROLL_TO_ITEM_END ||
          targetScrollTop >= maxScrollTop
        ) {
          virtualizedListRef.current?.scrollTo(Number.MAX_SAFE_INTEGER);
        } else {
          virtualizedListRef.current?.scrollTo(Math.round(clampedTarget));
        }
        flashScrollbar();
        return;
      }

      smoothScrollState.current = {
        active: true,
        start: Date.now(),
        from: startScrollTop,
        to: clampedTarget,
        duration,
        timer: setInterval(() => {
          const now = Date.now();
          const elapsed = now - smoothScrollState.current.start;
          const progress = Math.min(elapsed / duration, 1);

          // Ease-in-out
          const t = progress;
          const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;

          const current =
            smoothScrollState.current.from +
            (smoothScrollState.current.to - smoothScrollState.current.from) *
              ease;

          if (progress >= 1) {
            if (
              targetScrollTop === SCROLL_TO_ITEM_END ||
              targetScrollTop >= maxScrollTop
            ) {
              virtualizedListRef.current?.scrollTo(Number.MAX_SAFE_INTEGER);
            } else {
              virtualizedListRef.current?.scrollTo(Math.round(current));
            }
            stopSmoothScroll();
            flashScrollbar();
          } else {
            virtualizedListRef.current?.scrollTo(Math.round(current));
          }
        }, ANIMATION_FRAME_DURATION_MS),
      };
    },
    [stopSmoothScroll, flashScrollbar],
  );

  useKeypress(
    (key: Key) => {
      if (keyMatchers[Command.SCROLL_UP](key)) {
        stopSmoothScroll();
        scrollByWithAnimation(-1);
        return true;
      } else if (keyMatchers[Command.SCROLL_DOWN](key)) {
        stopSmoothScroll();
        scrollByWithAnimation(1);
        return true;
      } else if (
        keyMatchers[Command.PAGE_UP](key) ||
        keyMatchers[Command.PAGE_DOWN](key)
      ) {
        const direction = keyMatchers[Command.PAGE_UP](key) ? -1 : 1;
        const scrollState = getScrollState();
        const maxScroll = Math.max(
          0,
          scrollState.scrollHeight - scrollState.innerHeight,
        );
        const current = smoothScrollState.current.active
          ? smoothScrollState.current.to
          : Math.min(scrollState.scrollTop, maxScroll);
        const innerHeight = scrollState.innerHeight;
        smoothScrollTo(current + direction * innerHeight);
        return true;
      } else if (keyMatchers[Command.SCROLL_HOME](key)) {
        smoothScrollTo(0);
        return true;
      } else if (keyMatchers[Command.SCROLL_END](key)) {
        smoothScrollTo(SCROLL_TO_ITEM_END);
        return true;
      }
      return false;
    },
    { isActive: hasFocus },
  );

  const hasFocusCallback = useCallback(() => hasFocus, [hasFocus]);

  const scrollableEntry = useMemo(
    () => ({
      // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
      ref: containerRef as React.RefObject<DOMElement>,
      getScrollState,
      scrollBy: scrollByWithAnimation,
      scrollTo: smoothScrollTo,
      hasFocus: hasFocusCallback,
      flashScrollbar,
    }),
    [
      getScrollState,
      hasFocusCallback,
      flashScrollbar,
      scrollByWithAnimation,
      smoothScrollTo,
    ],
  );

  useScrollable(scrollableEntry, true);

  return (
    <Box ref={containerRef} flexGrow={1} flexDirection="column" width={width}>
      <VirtualizedList
        ref={virtualizedListRef}
        {...props}
        scrollbar={scrollbar}
        scrollbarThumbColor={scrollbarColor}
        stableScrollback={stableScrollback}
      />
    </Box>
  );
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const ScrollableListWithForwardRef = forwardRef(ScrollableList) as <T>(
  props: ScrollableListProps<T> & { ref?: React.Ref<ScrollableListRef<T>> },
) => React.ReactElement;

export { ScrollableListWithForwardRef as ScrollableList };
