import clsx from 'clsx';
import Autoplay from 'embla-carousel-autoplay';
import useEmblaCarousel from 'embla-carousel-react';
import React, {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import { twMerge } from 'tailwind-merge';

import IndicatorDot from './IndicatorDot';

export type Props = React.HTMLAttributes<HTMLDivElement> & {
  autoplay?: boolean | number;
  id?: string;
  loop?: boolean;
  draggable?: boolean;
  navClassName?: string;
  showIndicators?: boolean;
  slides?: Record<string, React.ReactNode>;
  slideOrder?: string[];
  index?: number;
  onIndexInit?: (index: number, id?: string) => void;
  onIndexChange?: (index: number, id?: string) => void;
};

const DEFAULT_AUTOPLAY_DELAY = 7000;

// Auto-incrementing number to generate unioque IDs
let counter = 0;

const SlideCarousel: React.FC<Props> = (props) => {
  const {
    autoplay: autoplayOption = false,
    id: explicitId,
    loop = false,
    draggable = true,
    index,
    navClassName,
    onIndexInit,
    onIndexChange,
    showIndicators,
    slides = {},
    slideOrder = slides ? Object.keys(slides) : [],
    ...restProps
  } = props;

  const [autoplayEnabled, autoplayDelay] =
    typeof autoplayOption === 'boolean'
      ? [autoplayOption, DEFAULT_AUTOPLAY_DELAY]
      : [true, autoplayOption];

  const id = useMemo(() => explicitId ?? `carousel-${counter++}`, [explicitId]);

  const [emblaRef, emblaApi] = useEmblaCarousel(
    {
      watchDrag: draggable,
      duration: 20,
      loop,
      // startIndex: index, // DOES NOT WORK, disables carousel animation
    },
    autoplayEnabled
      ? [
          Autoplay({
            playOnInit: autoplayEnabled,
            delay: autoplayDelay,
            stopOnMouseEnter: true,
            stopOnInteraction: false, // allows autoplay resume on mouse leave
            stopOnFocusIn: true,
            stopOnLastSnap: false, // allows looping
          }),
        ]
      : undefined
  );

  const [isDragging, setIsDragging] = useState(false);

  const stateRef = useRef({
    index: index || 0,
    slides: slideOrder,
    isFirstSlide: true,
  });
  useEffect(() => {
    stateRef.current.index = index || 0;
    stateRef.current.slides = slideOrder;
  }, [index, slideOrder]);

  const handleIndexChange = useCallback(
    (index?: number) => {
      const { index: prevIndex, slides } = stateRef.current;
      const totalSlides = slides.length;
      const nextIndex =
        index != null
          ? Math.max(0, Math.min(index, totalSlides - 1))
          : totalSlides && (prevIndex + 1) % totalSlides;
      onIndexChange?.(nextIndex, slides[nextIndex]);
    },
    [onIndexChange]
  );

  // Controlled slide navigation
  useEffect(() => {
    if (index != null) {
      if (emblaApi) {
        emblaApi.scrollTo(index, stateRef.current.isFirstSlide);
        stateRef.current.isFirstSlide = false;
        // Log impression?
      }
    }
  }, [index, emblaApi]);

  // Update index on Embla Carousel events
  useEffect(() => {
    if (emblaApi) {
      let isPointerDown = false;

      // Init event fires when the carousel is created
      const onInit = () => {
        const nextIndex = emblaApi.selectedScrollSnap();
        onIndexInit?.(nextIndex, stateRef.current.slides[nextIndex]);
      };

      // Select event fires is when the current slide is decided
      const onSelect = () => {
        const nextIndex = emblaApi.selectedScrollSnap();
        onIndexChange?.(nextIndex, stateRef.current.slides[nextIndex]);
      };

      // Settle event fires when the carousel animation is complete
      const onSettle = () => {
        setIsDragging(false);
      };

      /**
       * Scroll events fire when the carousel is moving or animating
       */
      const onScroll = () => {
        // Do not treat scrolling without the pointer down as dragging
        if (isPointerDown) {
          setIsDragging(true);
        }

        // Prevent overscroll
        const {
          limit,
          target,
          location,
          offsetLocation,
          scrollTo,
          translate,
          scrollBody,
          options,
        } = emblaApi.internalEngine();

        let edge;

        if (limit.reachedMax(target.get())) edge = limit.max;
        if (limit.reachedMin(target.get())) edge = limit.min;

        if (!options.loop && edge !== undefined) {
          offsetLocation.set(edge);
          location.set(edge);
          target.set(edge);
          translate.to(edge);
          translate.toggleActive(false);
          scrollBody.useDuration(0).useFriction(0);
          scrollTo.distance(0, false);
        } else {
          translate.toggleActive(true);
        }
      };

      const onPointerDown = () => {
        isPointerDown = true;
        setIsDragging(false);
      };
      const onPointerUp = () => {
        isPointerDown = false;
        setIsDragging(false);
      };

      emblaApi
        .on('init', onInit)
        .on('reInit', onSelect)
        .on('select', onSelect)
        .on('scroll', onScroll)
        .on('settle', onSettle)
        .on('pointerDown', onPointerDown)
        .on('pointerUp', onPointerUp);

      return () => {
        emblaApi
          .off('init', onInit)
          .off('reInit', onSelect)
          .off('select', onSelect)
          .off('scroll', onScroll)
          .off('settle', onSettle)
          .off('pointerDown', onPointerDown)
          .off('pointerUp', onPointerUp);
      };
    }
  }, [onIndexChange, onIndexInit, emblaApi]);

  return (
    <div
      {...restProps}
      className={twMerge(
        'flex flex-col items-stretch justify-start',
        'h-full w-full',
        props.className
      )}
    >
      <div className='relative flex-1 overflow-clip' ref={emblaRef}>
        <div
          className={clsx('absolute inset-0 flex flex-row', {
            'select-none': draggable && isDragging,
          })}
        >
          {slideOrder.map((key, i) => (
            <div
              key={key}
              className={clsx(
                '@container relative min-h-full w-full flex-none basis-full',
                {
                  'pointer-none overflow-clip': i !== index,
                  'overflow-x-clip overflow-y-auto': i === index,
                }
              )}
              aria-hidden={i !== index}
              aria-labelledby={`${id}-button-${i}`}
            >
              {slides[key]}
            </div>
          ))}
        </div>
      </div>
      <div
        className={twMerge(
          clsx('pointer-events-none absolute inset-x-0 bottom-0', {
            hidden:
              showIndicators === false ||
              (showIndicators !== true && slideOrder.length < 2),
          }),
          navClassName
        )}
      >
        <nav className='pointer-events-auto mx-auto flex w-max flex-row items-center justify-center p-2'>
          {slideOrder.map((key, i) => (
            <IndicatorDot
              key={key}
              active={i === index}
              id={`${id}-button-${i}`}
              onClick={
                emblaApi &&
                (() => {
                  emblaApi.scrollTo(i);
                  handleIndexChange(i);
                })
              }
              aria-label={`Go to slide ${i + 1}`}
            />
          ))}
        </nav>
      </div>
    </div>
  );
};

const UncontrolledSlideCarousel: React.FC<
  Props & {
    defaultIndex?: number;
  }
> = (props) => {
  const { defaultIndex, onIndexChange, ...restProps } = props;

  const [index, setIndex] = useState(defaultIndex || 0);
  const handleIndexChange = useCallback(
    (nextIndex: number) => {
      setIndex(nextIndex);
      onIndexChange?.(nextIndex);
    },
    [onIndexChange]
  );

  return (
    <SlideCarousel
      index={index}
      onIndexChange={handleIndexChange}
      {...restProps}
    />
  );
};

export default UncontrolledSlideCarousel;
