import styled from '@emotion/styled';
import {
  RefObject,
  memo,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';

import useClip from '@/hooks/useClip';
import { useContextSelector } from '@/hooks/useContextSelector';
import { ArrowUpIcon, CaretLeftIcon, CaretRightIcon, CloseIcon } from '@/icons';

import IntersectionTrigger from '../IntersectionTrigger';
import Button, { ButtonShape, ButtonVariant } from '../button/Button';
import { ResponsiveChild, useContainer } from '../containerQueries/components';
import SpinnerSVG from '../svg/SpinnerSVG';
import { Tooltip } from '../tooltip/Tooltip';
import JustCreatedClips from './JustCreatedClips';
import { MultiSelectContextProvider } from './MultiSelectContext';
import { ClipBrowserContext } from './useClipBrowser';

const PositionWrapper = styled.div<{ hideScrollbar?: boolean }>`
  flex-grow: 1;
  position: relative;
  height: 100%;
`;

const PageControls = styled.div<{
  fadeOut: boolean;
  nudgeLeft: boolean;
}>`
  position: absolute;
  display: flex;
  align-items: center;
  gap: 8px;
  bottom: 16px;
  left: calc(50% - ${({ nudgeLeft }) => (nudgeLeft ? '24px' : '0px')});
  transform: translateX(-50%);
  z-index: 1000;
  border-radius: 2000px;
  padding: 8px;
  background-color: var(--color-background-secondary);
  border: 1px solid var(--color-border-primary);
  opacity: ${({ fadeOut }) => (fadeOut ? 0 : 1)};
  pointer-events: ${({ fadeOut }) => (fadeOut ? 'none' : 'auto')};
  transition: opacity 0.2s ease-in-out;
`;

const ScrollWrapper = styled.div<{ hideScrollbar?: boolean }>`
  flex-grow: 1;
  height: 100%;
  overflow-y: auto;
  ${({ hideScrollbar }) =>
    hideScrollbar &&
    `
    scrollbar-width: none;
    -ms-overflow-style: none;
    &::-webkit-scrollbar {
      display: none;
    }
  `}
`;

const Clips = styled.div<{ stale: boolean }>`
  opacity: ${(props) => (props.stale ? 0.5 : 1)};
  transition: opacity 0.2s ease-in-out;
  overflow: hidden;
  > * + * {
    margin-top: 1px;
  }
`;

const AfterClips = styled.div`
  padding: 16px 0;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  margin-bottom: 107px;

  .intersection-trigger {
    pointer-events: none;
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: 200vh;
  }
`;

const EndOfListMessage = styled.div`
  padding: 16px 0;
  text-align: center;
  opacity: 0.5;
  font-size: 14px;
`;

const ScrollToTopButton = styled.div`
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 1000;
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 8px;
  padding-bottom: 64px;
  gap: 8px;
  animation: fadeIn 0.5s;
  background-image: linear-gradient(
    to bottom,
    var(--color-background-primary),
    transparent
  );
  button {
    animation: fade-up 0.5s;
  }
`;

const UnseenClipsDot = styled.div`
  position: absolute;
  top: -12px;
  right: -12px;
  background-color: var(--color-accent-brand);
  color: var(--color-foreground-primary);
  border-radius: 50%;
  padding: 4px;
  min-width: 16px;
  height: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
`;

const PageInput = styled.input`
  background-color: var(--color-background-glass-thick);
  color: var(--color-foreground-primary);
  padding: 8px;
  border-radius: 8px;
  text-align: center;
  width: 60px;
  border-radius: 1000px;
  height: 40px;
`;

const useNumUnseenClips = (
  clips: string[],
  scrollWrapperRef: RefObject<HTMLDivElement | null>,
  clipRowListRef: RefObject<HTMLDivElement | null>
) => {
  const isFirstClipVisible = useCallback(() => {
    const scrollWrapper = scrollWrapperRef.current;
    const clipsWrapper = clipRowListRef.current;
    if (!scrollWrapper || !clipsWrapper) return false;
    const firstClipRow = clipsWrapper.children[0];
    if (!firstClipRow) return false;
    const firstClipRowHeight = firstClipRow.clientHeight;
    const scrollTop = scrollWrapper.scrollTop;
    return scrollTop <= firstClipRowHeight;
  }, []);

  const { clip: firstClip } = useClip(clips[0] ?? null);
  const lastFirstClipId = useRef<string | null>(clips[0] ?? null);
  const numNewClipsRef = useRef(0);

  const lastClips = useRef<string[]>(clips);
  useEffect(() => {
    let numNewClips = 0;
    for (let i = 0; i < clips.length; i++) {
      const clipId = clips[i];
      if (!lastClips.current.includes(clipId)) {
        numNewClips++;
      } else {
        break;
      }
    }
    numNewClipsRef.current = numNewClips;
    lastClips.current = clips;
  }, [clips]);

  const [numUnseenClips, setNumUnseenClips] = useState(0);

  useEffect(() => {
    const firstClipId = firstClip?.id ?? null;
    const firstClipStatus = firstClip?.status ?? '';
    const firstClipIsVisible = isFirstClipVisible();
    if (
      firstClipId !== lastFirstClipId.current &&
      ['submitted', 'queued', 'streaming'].includes(firstClipStatus) &&
      !firstClipIsVisible
    ) {
      setNumUnseenClips(numNewClipsRef.current);
    }
    lastFirstClipId.current = firstClipId;
  }, [firstClip, numNewClipsRef, isFirstClipVisible]);

  useEffect(() => {
    const onScroll = () => {
      if (isFirstClipVisible()) {
        setNumUnseenClips(0);
      }
    };
    scrollWrapperRef.current?.addEventListener('scroll', onScroll);
    return () => {
      scrollWrapperRef.current?.removeEventListener('scroll', onScroll);
    };
  }, [isFirstClipVisible, setNumUnseenClips]);

  const resetNumUnseenClips = useCallback(() => {
    setNumUnseenClips(0);
  }, []);

  return useMemo(
    () => [numUnseenClips, resetNumUnseenClips] as const,
    [numUnseenClips, resetNumUnseenClips]
  );
};

export default memo(function ClipBrowserList({
  ListComponent,
  JustCreatedListComponent,
  hideScrollbar,
  hidePageControls,
  clip1OnboardingRef,
  likeOnboardingRef,
  shareOnboardingRef,
  scrollContainerRef,
}: {
  ListComponent: React.ComponentType<{
    clipId: string;
    onboardingRef?: React.RefObject<HTMLDivElement | null>;
    likeOnboardingRef?: React.RefObject<HTMLDivElement | null>;
    shareOnboardingRef?: React.RefObject<HTMLDivElement | null>;
  }>;
  JustCreatedListComponent?: React.ComponentType<{ clipId: string }>;
  hideScrollbar?: boolean;
  hidePageControls?: boolean;
  clip1OnboardingRef?: React.RefObject<HTMLDivElement | null>;
  likeOnboardingRef?: React.RefObject<HTMLDivElement | null>;
  shareOnboardingRef?: React.RefObject<HTMLDivElement | null>;
  scrollContainerRef?: RefObject<HTMLDivElement | null>;
}) {
  const clips = useContextSelector(
    ClipBrowserContext,
    (context) => context.clips
  );
  const fetchNextPage = useContextSelector(
    ClipBrowserContext,
    (context) => context.query.fetchNextPage
  );
  const hasNextPage = useContextSelector(
    ClipBrowserContext,
    (context) => context.query.hasNextPage
  );
  const isFetching = useContextSelector(
    ClipBrowserContext,
    (context) => context.query.isFetching
  );
  const applyingNewFilters = useContextSelector(
    ClipBrowserContext,
    (context) => context.applyingNewFilters
  );
  const isPageMode = useContextSelector(
    ClipBrowserContext,
    (context) => context.isPageMode
  );
  const canResetFilters = useContextSelector(
    ClipBrowserContext,
    (context) => context.canResetFilters
  );
  const resetFilters = useContextSelector(
    ClipBrowserContext,
    (context) => context.resetFilters
  );
  const scrollWrapperRef = useRef<HTMLDivElement>(null);
  const clipRowListRef = useRef<HTMLDivElement>(null);

  // Sync the internal ref with the external one if provided
  useEffect(() => {
    if (scrollContainerRef && scrollWrapperRef.current) {
      (
        scrollContainerRef as React.MutableRefObject<HTMLDivElement | null>
      ).current = scrollWrapperRef.current;
    }
  }, [scrollContainerRef]);

  const { containerName, containerRef } = useContainer();

  useEffect(() => {
    if (scrollWrapperRef.current) {
      scrollWrapperRef.current.scrollTo({ top: 0, behavior: 'instant' });
    }
  }, [applyingNewFilters]);

  const scrollToTop = useCallback(() => {
    if (scrollWrapperRef.current) {
      scrollWrapperRef.current.scrollTo({ top: 0, behavior: 'smooth' });
    }
  }, []);

  const [pageUpdatedRecently, setPageUpdatedRecently] = useState(false);

  const [numUnseenClips, resetNumUnseenClips] = useNumUnseenClips(
    clips,
    scrollWrapperRef,
    clipRowListRef
  );

  const setPageNumber = useContextSelector(
    ClipBrowserContext,
    (context) => context.setPageNumber
  );

  const currentPageNumber = useContextSelector(
    ClipBrowserContext,
    (context) => context.currentPageNumber
  );

  const exitPageMode = useContextSelector(
    ClipBrowserContext,
    (context) => context.exitPageMode
  );

  const filters = useContextSelector(
    ClipBrowserContext,
    (context) => context.filters
  );

  const pageInputRef = useRef<HTMLInputElement>(null);

  const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);

  const pageLimit = useContextSelector(
    ClipBrowserContext,
    (context) => context.pageLimit
  );

  const getShownPage = useCallback(() => {
    const scrollWrapper = scrollWrapperRef.current;
    if (!scrollWrapper) return undefined;
    const list = clipRowListRef.current;
    if (!list) return undefined;

    // Calculate how many rows are scrolled off the top
    const scrollTop = scrollWrapper.scrollTop;
    const firstChild = list.children[0] as HTMLElement;
    if (!firstChild) return undefined;

    const rowHeight = firstChild.offsetHeight + 1; // +1 for margin-top
    const rowsOffTop = Math.floor(scrollTop / rowHeight);

    // Calculate page number
    const currentPage = Math.floor(rowsOffTop / pageLimit) + 1;
    return currentPage;
  }, [pageLimit]);

  const pageUpdated = useCallback(() => {
    setPageUpdatedRecently(true);
    if (scrollTimeoutRef.current) {
      clearTimeout(scrollTimeoutRef.current);
      scrollTimeoutRef.current = null;
    }
    scrollTimeoutRef.current = setTimeout(() => {
      setPageUpdatedRecently(false);
    }, 3000);
  }, []);

  useEffect(() => {
    const scrollWrapper = scrollWrapperRef.current;
    if (!scrollWrapper) return;

    const updateInput = () => {
      if (isPageMode) return;
      const pageInput = pageInputRef.current;
      if (!pageInput) return;
      const currentPage = getShownPage();
      if (currentPage === undefined) return;
      pageInput.value = currentPage.toString();
    };

    updateInput();

    const handleScroll = () => {
      pageUpdated();
      updateInput();
    };

    scrollWrapper.addEventListener('scroll', handleScroll);
    return () => {
      scrollWrapper.removeEventListener('scroll', handleScroll);
    };
  }, [isPageMode, pageUpdated, getShownPage]);

  useEffect(() => {
    pageUpdated();
    if (isPageMode) {
      const pageInput = pageInputRef.current;
      if (pageInput) {
        pageInput.value = currentPageNumber?.toString() ?? '';
      }
    }
  }, [currentPageNumber, isPageMode, pageUpdated]);

  const goToNextPage = useCallback(() => {
    const pageNumber = currentPageNumber ?? getShownPage() ?? 0;
    setPageNumber(pageNumber + 1);
    const pageInput = pageInputRef.current;
    if (pageInput) {
      pageInput.value = (pageNumber + 1).toString();
    }
  }, [currentPageNumber, setPageNumber]);

  const goToPreviousPage = useCallback(() => {
    const pageNumber = currentPageNumber ?? getShownPage() ?? 0;
    const newPageNumber = Math.max(1, pageNumber - 1);
    setPageNumber(newPageNumber);
    const pageInput = pageInputRef.current;
    if (pageInput) {
      pageInput.value = newPageNumber.toString();
    }
  }, [currentPageNumber, setPageNumber]);

  const resetPageInput = useCallback(() => {
    const pageInput = pageInputRef.current;
    if (pageInput) {
      pageInput.value = '1';
    }
  }, []);

  useEffect(() => {
    resetPageInput();
  }, [filters, resetPageInput]);

  const [focusedPageInput, setFocusedPageInput] = useState(false);

  return (
    <PositionWrapper ref={containerRef}>
      {JustCreatedListComponent && (
        <JustCreatedClips ListComponent={JustCreatedListComponent} />
      )}
      {numUnseenClips > 0 && (
        <ScrollToTopButton>
          <ResponsiveChild containerName={containerName} hideBelowWidth={180}>
            <Button
              variant={ButtonVariant.Primary}
              shape={ButtonShape.Pill}
              onClick={() => {
                scrollToTop();
                resetNumUnseenClips();
              }}
              icon={<ArrowUpIcon />}
            >
              Show new clips
              <UnseenClipsDot>{numUnseenClips}</UnseenClipsDot>
            </Button>
          </ResponsiveChild>

          <ResponsiveChild containerName={containerName} showBelowWidth={180}>
            <Tooltip label='Show new clips' placement='bottom'>
              <Button
                variant={ButtonVariant.Primary}
                shape={ButtonShape.Pill}
                onClick={() => {
                  scrollToTop();
                  resetNumUnseenClips();
                }}
                icon={
                  <>
                    <ArrowUpIcon />
                    <UnseenClipsDot>{numUnseenClips}</UnseenClipsDot>
                  </>
                }
              />
            </Tooltip>
          </ResponsiveChild>
        </ScrollToTopButton>
      )}
      <ScrollWrapper
        ref={scrollWrapperRef}
        hideScrollbar={hideScrollbar}
        className='clip-browser-list-scroller'
      >
        <MultiSelectContextProvider>
          <Clips
            stale={applyingNewFilters}
            ref={clipRowListRef}
            role='rowgroup'
          >
            {clips.map((clipId, index) => (
              <ListComponent
                key={clipId}
                clipId={clipId}
                onboardingRef={index === 0 ? clip1OnboardingRef : undefined}
                likeOnboardingRef={index === 0 ? likeOnboardingRef : undefined}
                shareOnboardingRef={
                  index === 0 ? shareOnboardingRef : undefined
                }
              />
            ))}
          </Clips>
          <AfterClips>
            {isFetching || applyingNewFilters ? (
              <>
                <SpinnerSVG />
                {canResetFilters && (
                  <Button
                    variant={ButtonVariant.Primary}
                    shape={ButtonShape.Pill}
                    onClick={resetFilters}
                    className='mt-4'
                  >
                    Reset filters
                  </Button>
                )}
              </>
            ) : hasNextPage ? (
              <>
                <IntersectionTrigger
                  onTrigger={fetchNextPage}
                  className='intersection-trigger'
                />
              </>
            ) : (
              <EndOfListMessage>
                <div>
                  {clips.length === 0
                    ? 'No songs found'
                    : isPageMode
                      ? `Showing one page of ${clips.length} songs`
                      : `${clips.length} songs`}
                </div>
                <div className='mt-4'>
                  <Button shape={ButtonShape.Pill} onClick={resetFilters}>
                    Reset filters
                  </Button>
                </div>
              </EndOfListMessage>
            )}
          </AfterClips>
        </MultiSelectContextProvider>
      </ScrollWrapper>
      {!hidePageControls && (
        <PageControls
          fadeOut={!pageUpdatedRecently && !focusedPageInput && !isPageMode}
          nudgeLeft={!isPageMode}
        >
          <span className='mr-2 ml-4'>Page</span>
          <Button
            shape={ButtonShape.Pill}
            variant={
              !isPageMode || currentPageNumber === 1
                ? ButtonVariant.Tertiary
                : ButtonVariant.LightGlass
            }
            onClick={goToPreviousPage}
            className={`p-3`}
            icon={<CaretLeftIcon className='h-4 w-4' />}
            disabled={!isPageMode || currentPageNumber === 1}
          />
          <PageInput
            defaultValue={currentPageNumber?.toString() ?? '1'}
            placeholder={currentPageNumber?.toString() ?? '#'}
            ref={pageInputRef}
            onFocus={() => {
              setFocusedPageInput(true);
            }}
            onBlur={() => {
              setFocusedPageInput(false);
            }}
            onKeyDown={(e) => {
              if (e.key === 'Enter') {
                const pageNumber = parseInt(e.currentTarget.value);
                if (!Number.isNaN(pageNumber) && pageNumber > 0) {
                  setPageNumber(pageNumber);
                } else {
                  exitPageMode();
                  e.currentTarget.blur();
                  resetPageInput();
                }
              } else if (e.key === 'Escape') {
                e.currentTarget.blur();
              }
            }}
          />
          <Button
            shape={ButtonShape.Pill}
            variant={ButtonVariant.LightGlass}
            onClick={goToNextPage}
            className='p-3'
            icon={<CaretRightIcon className='h-4 w-4' />}
          />
          {isPageMode && (
            <Button
              shape={ButtonShape.Pill}
              variant={
                isPageMode ? ButtonVariant.Tertiary : ButtonVariant.LightGlass
              }
              onClick={() => {
                exitPageMode();
                scrollToTop();
                resetPageInput();
              }}
              className='p-3'
              icon={<CloseIcon className='h-4 w-4' />}
            />
          )}
        </PageControls>
      )}
    </PositionWrapper>
  );
});
