/* eslint jsx-a11y/click-events-have-key-events: warn */

/* eslint jsx-a11y/no-static-element-interactions: warn */
import clsx from 'clsx';
import useEmblaCarousel, { UseEmblaCarouselType } from 'embla-carousel-react';
import { clamp } from 'lodash-es';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { twMerge } from 'tailwind-merge';

import Button, {
  ButtonShape,
  ButtonSize,
  ButtonVariant,
} from '@/components/button/Button';
import useDeviceAttributes from '@/hooks/useDeviceAttributes';
import { ChevronLeftIcon, ChevronRightIcon } from '@/icons';
import { WheelGesturesPlugin } from '@/utils/EmblaCarouselWheelGestures';
import { TYPOGRAPHY_DISCOVER_SECTION_TITLE_CLASSNAME } from '@/utils/constants';

type EmblaOptionsType = NonNullable<Parameters<typeof useEmblaCarousel>[0]>;

export type DiscoverCarouselProps<
  I extends object,
  P extends object = object,
> = {
  headerClassName?: string;
  titleClassName?: string;
  descriptionClassName?: string;
  actionsClassName?: string;
  carouselClassName?: string;
  itemClassName?: string;
  itemsContainerClassName?: string;
  maskClassName?: string;
  buttonClassName?: string;
  buttonVariant?: ButtonVariant;
  buttonShape?: ButtonShape;
  buttonSize?: ButtonSize;
  buttonVerticalPosition?: string | number;
  id?: string;
  name?: string | null;
  title?: string | null;
  description?: string | null;
  actions?: React.ReactNode;
  items?: I[];
  contentBefore?: React.ReactNode;
  contentAfter?: React.ReactNode;
  carouselAlign?: EmblaOptionsType['align'];
  carouselStartIndex?: EmblaOptionsType['startIndex'];
  allowOverscroll?: boolean;
  onCarouselLeftClick?: ((emblaApi: UseEmblaCarouselType[1]) => void) | null;
  onCarouselRightClick?: ((emblaApi: UseEmblaCarouselType[1]) => void) | null;
  onScrollStateChange?: (maskLeft: number, maskRight: number) => void;
  renderItem: React.ComponentType<
    | {
        item: I;
        className?: string;
        index: number;
      }
    | ({
        item: I;
        className?: string;
        index: number;
      } & P)
  >;
  renderItemProps?: P;
  skeletonCount?: number;
  renderSkeleton?: (index: number) => React.ReactNode;
} & Pick<
  EmblaOptionsType,
  'loop' | 'dragFree' | 'skipSnaps' | 'inViewThreshold'
>;

export type Props<I extends object, P extends object> = Omit<
  React.HTMLAttributes<HTMLDivElement>,
  keyof DiscoverCarouselProps<I, P>
> &
  DiscoverCarouselProps<I, P>;

const SCROLL_MASK_THRESHOLD = 50;

export function scrollToPrevNotInView(emblaApi: UseEmblaCarouselType[1]) {
  if (emblaApi) {
    const firstSlideInView = emblaApi.slidesInView().shift();
    if (firstSlideInView !== undefined) {
      emblaApi.scrollTo(
        emblaApi.internalEngine().index.set(firstSlideInView).add(-1).get()
      );
    } else {
      emblaApi.scrollPrev();
    }
  }
}

export function scrollToNextInView(emblaApi: UseEmblaCarouselType[1]) {
  if (emblaApi) {
    const lastSlideInView = emblaApi.slidesInView().pop();
    if (lastSlideInView !== undefined) {
      emblaApi.scrollTo(
        emblaApi.internalEngine().index.set(lastSlideInView).add(1).get()
      );
    } else {
      emblaApi.scrollNext();
    }
  }
}

export const DiscoverCarousel = <I extends object, P extends object>(
  props: Props<I, P>
) => {
  const {
    className,
    headerClassName,
    titleClassName,
    descriptionClassName,
    actionsClassName,
    carouselClassName,
    itemClassName,
    itemsContainerClassName,
    maskClassName = 'max-sm:mask-none',
    buttonClassName,
    buttonVariant = ButtonVariant.Standard,
    buttonShape = ButtonShape.Pill,
    buttonSize = ButtonSize.Small,
    buttonVerticalPosition,
    id,
    name,
    title,
    description,
    actions,
    items,
    contentBefore,
    contentAfter,
    renderItem,
    renderItemProps,
    skeletonCount = 3,
    renderSkeleton,
    allowOverscroll = false,
    carouselAlign = 'center',
    dragFree,
    inViewThreshold = 1,
    loop = false,
    skipSnaps,
    carouselStartIndex = 0,
    onCarouselLeftClick,
    onCarouselRightClick,
    onScrollStateChange,
    ...restProps
  } = props;

  const { t } = useTranslation();

  const { isMobile } = useDeviceAttributes();

  const [emblaRef, emblaApi] = useEmblaCarousel(
    {
      align: carouselAlign,
      axis: 'x',
      containScroll: 'keepSnaps',
      direction: 'ltr',
      dragFree: dragFree ?? !isMobile,
      inViewThreshold,
      loop,
      skipSnaps: skipSnaps ?? !isMobile,
      startIndex: carouselStartIndex,
      // `WheelGesturesPlugin` simulates mouse events, so assume that any
      // events that are untrusted and on the container are actually scroll
      // wheel events in disguise and ignore for the sake of dragging
      watchDrag: (emblaApi, e) =>
        isMobile || (!e.isTrusted && emblaApi.containerNode() === e.target),
    },
    [WheelGesturesPlugin({ allowOverscroll })]
  );
  // Update index on Embla Carousel events
  useEffect(() => {
    if (emblaApi) {
      const updateMask = () => {
        const { limit, location, target } = emblaApi.internalEngine();
        const currentPosition = Math.round(location.get());
        const targetPosition = Math.round(target.get());
        const nextMaskLeft =
          Math.max(limit.max - currentPosition, limit.max - targetPosition) /
          SCROLL_MASK_THRESHOLD;
        const nextMaskRight =
          Math.max(currentPosition - limit.min, targetPosition - limit.min) /
          SCROLL_MASK_THRESHOLD;
        const clampedLeft = clamp(nextMaskLeft, 0, 1);
        const clampedRight = clamp(nextMaskRight, 0, 1);
        setMask([clampedLeft, clampedRight]);
        onScrollStateChange?.(clampedLeft, clampedRight);
        return [nextMaskLeft, nextMaskRight] as const;
      };

      /**
       * Scroll events fire when the carousel is moving or animating
       */
      let frictionDurationOverride = false;
      const onScroll = () => {
        const {
          limit,
          target,
          previousLocation,
          location,
          offsetLocation,
          scrollTo,
          translate,
          scrollBody,
          options,
        } = emblaApi.internalEngine();

        updateMask();

        // Prevent overscroll
        if (!allowOverscroll) {
          if (!options.loop) {
            // Clamp the target to the current bounds
            if (limit.reachedMax(target.get())) {
              target.set(limit.max);
            } else if (limit.reachedMin(target.get())) {
              target.set(limit.min);
            }
            // Hard snap if we're out of bounds
            const currentLocation = location.get();
            let edge: number | undefined;
            if (currentLocation >= limit.max) edge = limit.max;
            if (currentLocation <= limit.min) edge = limit.min;
            if (edge !== undefined) {
              previousLocation.set(edge);
              location.set(edge);
              offsetLocation.set(edge);
              target.set(edge);
              translate.to(edge);
              scrollBody.useFriction(1).useDuration(0);
              scrollTo.distance(0, false);
              translate.toggleActive(false);
              frictionDurationOverride = true;
            } else {
              if (frictionDurationOverride) {
                scrollBody.useBaseDuration().useBaseFriction();
                translate.toggleActive(true);
                frictionDurationOverride = false;
              }
            }
          }
        }
      };

      emblaApi
        .on('init', updateMask)
        .on('settle', updateMask)
        .on('resize', updateMask)
        .on('scroll', onScroll);

      // Force mask update, since the effect probably missed `init`
      updateMask();

      return () => {
        emblaApi
          .off('init', updateMask)
          .off('settle', updateMask)
          .off('resize', updateMask)
          .off('scroll', onScroll);
      };
    }
  }, [allowOverscroll, emblaApi]);

  if (typeof window !== 'undefined') {
    (window as any).emblaApi = emblaApi;
  }

  const [[maskLeft, maskRight], setMask] = useState([0, 1]);

  const handleCarouselLeftClick = useCallback(
    (e: React.MouseEvent) => {
      if (onCarouselLeftClick) {
        onCarouselLeftClick(emblaApi);
      } else {
        scrollToPrevNotInView(emblaApi);
      }
      e.stopPropagation();
    },
    [emblaApi, onCarouselLeftClick]
  );
  const handleCarouselRightClick = useCallback(
    (e: React.MouseEvent) => {
      if (onCarouselRightClick) {
        onCarouselRightClick(emblaApi);
      } else {
        scrollToNextInView(emblaApi);
      }
      e.stopPropagation();
    },
    [emblaApi, onCarouselRightClick]
  );

  return (
    <div className={twMerge('flex flex-col gap-4', className)} {...restProps}>
      <div
        className={twMerge(
          'flex flex-row items-center justify-start gap-2',
          headerClassName
        )}
      >
        <div className='flex-1'>
          {title && (
            <h1
              className={twMerge(
                TYPOGRAPHY_DISCOVER_SECTION_TITLE_CLASSNAME,
                'text-foreground-primary',
                titleClassName
              )}
            >
              {title}
            </h1>
          )}
          {description && (
            <p
              className={twMerge(
                'font-sans text-[14px] leading-[16px] font-semibold text-foreground-tertiary',
                descriptionClassName
              )}
            >
              {description}
            </p>
          )}
        </div>
        {actions && <div className={actionsClassName}>{actions}</div>}
      </div>
      {items?.length ? (
        <div
          className={twMerge(
            'relative flex-1 overflow-clip max-sm:-mx-4 max-sm:px-4',
            carouselClassName
          )}
          style={
            {
              '--carousel-mask-left': `${(100 - maskLeft * 10).toFixed(2)}%`,
              '--carousel-mask-right': `${(100 - maskRight * 10).toFixed(2)}%`,
            } as React.CSSProperties
          }
        >
          <div
            className={twMerge(
              clsx(
                'w-full',
                { 'mask-l-from-(--carousel-mask-left)': maskLeft > 0 },
                { 'mask-r-from-(--carousel-mask-right)': maskRight > 0 },
                maskClassName
              )
            )}
          >
            <div ref={emblaRef}>
              <div
                className={twMerge(
                  'flex flex-row gap-4',
                  itemsContainerClassName
                )}
              >
                {contentBefore}
                {items.map((item, index) =>
                  React.createElement(renderItem, {
                    key: index,
                    index,
                    item,
                    className: itemClassName,
                    ...renderItemProps,
                  })
                )}
                {contentAfter}
              </div>
            </div>
          </div>
          {onCarouselLeftClick === null ? null : (
            <div
              className={clsx(
                'absolute left-1 z-20 flex items-center justify-center',
                'transition-[transform,opacity] duration-200',
                'max-sm:hidden',
                {
                  'pointer-events-none translate-x-4 scale-0 opacity-0':
                    maskLeft <= 0,
                },
                buttonVerticalPosition ? '' : 'inset-y-0'
              )}
              style={
                buttonVerticalPosition
                  ? {
                      top:
                        typeof buttonVerticalPosition === 'number'
                          ? `${buttonVerticalPosition}rem`
                          : buttonVerticalPosition,
                      transform: 'translateY(-50%)',
                    }
                  : {}
              }
              onClick={handleCarouselLeftClick}
            >
              <Button
                className={buttonClassName}
                variant={buttonVariant}
                shape={buttonShape}
                size={buttonSize}
                icon={ChevronLeftIcon}
                onClick={handleCarouselLeftClick}
                aria-label={t('cta.prev')}
                aria-hidden={maskLeft > 0}
              />
            </div>
          )}
          {onCarouselRightClick === null ? null : (
            <div
              className={clsx(
                'absolute right-1 z-20 flex items-center justify-center',
                'transition-[transform,opacity] duration-200',
                'max-sm:hidden',
                {
                  'pointer-events-none -translate-x-4 scale-0 opacity-0':
                    maskRight <= 0,
                },
                buttonVerticalPosition ? '' : 'inset-y-0'
              )}
              style={
                buttonVerticalPosition
                  ? {
                      top:
                        typeof buttonVerticalPosition === 'number'
                          ? `${buttonVerticalPosition}rem`
                          : buttonVerticalPosition,
                      transform: 'translateY(-50%)',
                    }
                  : {}
              }
              onClick={handleCarouselRightClick}
            >
              <Button
                className={buttonClassName}
                variant={buttonVariant}
                shape={buttonShape}
                size={buttonSize}
                icon={ChevronRightIcon}
                onClick={handleCarouselRightClick}
                aria-label={t('cta.next')}
                aria-disabled={maskRight > 0}
              />
            </div>
          )}
        </div>
      ) : renderSkeleton ? (
        <div className='flex flex-row gap-4 overflow-hidden'>
          {[...Array(skeletonCount)].map((_, i) => (
            <React.Fragment key={i}>{renderSkeleton(i)}</React.Fragment>
          ))}
        </div>
      ) : null}
    </div>
  );
};

export default DiscoverCarousel;
