'use client';

import {
  Slider,
  SliderFilledTrack,
  SliderThumb,
  SliderTrack,
} from '@chakra-ui/react';
import clsx from 'clsx';
import { debounce } from 'lodash-es';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import AvatarEditor from 'react-avatar-editor';
import { Accept, DropzoneProps, useDropzone } from 'react-dropzone';
import { twMerge } from 'tailwind-merge';

import Button, {
  ButtonShape,
  ButtonSize,
  ButtonVariant,
} from '@/components/button/Button';
import { TrashIcon, UploadIcon } from '@/icons';
import { normalizeToScale } from '@/utils/utils';

const DEFAULT_WIDTH = 200;
const DEFAULT_HEIGHT = 200;
const SMALLER_WIDTH = 150;
const SMALLER_HEIGHT = 150;

const ACCEPT_FILE_TYPES: Accept = {
  'image/jpeg': ['.jpeg', '.jpg'],
  'image/png': ['.png'],
  'image/webp': ['.webp'],
  'image/bmp': ['.bmp'],
};

export type Props = Pick<
  DropzoneProps,
  'accept' | 'minSize' | 'maxSize' | 'disabled'
> & {
  className?: string;
  uploaderClassName?: string;
  innerClassName?: string;
  style?: React.CSSProperties;
  children?: React.ReactNode;
  initialImageURL?: string;
  onImageChanged: (imageData: string | null) => void;
  /**
   * Width of the cropped image
   */
  width?: number;
  /**
   * Height of the cropped image
   */
  height?: number;
  /**
   * Displayed width of the crop area
   */
  innerWidth?: number;
  /**
   * Display height of the crop area
   */
  innerHeight?: number;
  /**
   * Shows part of the image outside the crop area
   */
  paddingX?: number;
  /**
   * Shows part of the image outside the crop area
   */
  paddingY?: number;
  defaultScale?: number;
  minScale?: number;
  maxScale?: number;
  smallerSize?: boolean;
  debounceAmount?: number;
  buttonContainerClassName?: string;
  sliderContainerClassName?: string;
  uploaderIconClassName?: string;
  uploaderWrapperClassName?: string;
  uploaderTextClassName?: string;
};

const CropFrame: React.FC<
  React.SVGAttributes<SVGSVGElement> & {
    width: number;
    height: number;
    innerWidth?: number;
    innerHeight?: number;
    innerOffsetX?: number;
    innerOffsetY?: number;
    innerRadius?: number;
  }
> = (props) => {
  const {
    width,
    height,
    innerWidth: w = width,
    innerHeight: h = height,
    innerOffsetX: x = 0.5 * (width - w),
    innerOffsetY: y = 0.5 * (height - h),
    innerRadius: r = 0,
    ...restProps
  } = props;

  const path = [
    `M0,0 h${width} v${height} h${-width} Z`,
    r
      ? [
          `M${x},${y + r}`,
          `a${r},${r} 0 0 1 ${r},${-r}`,
          `h${w - 2 * r}`,
          `a${r},${r} 0 0 1 ${r},${r}`,
          `v${h - 2 * r}`,
          `a${r},${r} 0 0 1 ${-r},${r}`,
          `h${2 * r - w}`,
          `a${r},${r} 0 0 1 ${-r},${-r}`,
        ].join(' ')
      : `M${x},${y} h${w} v${h} h${-w}`,
    'Z',
  ].join(' ');
  return (
    <svg
      viewBox={`0 0 ${width} ${height}`}
      fill='currentColor'
      preserveAspectRatio='none'
      {...restProps}
    >
      <path d={path} fillRule='evenodd' />
    </svg>
  );
};

const ImageUploader: React.FC<Props> = (props) => {
  const {
    className,
    uploaderClassName,
    innerClassName,
    style,
    children,
    initialImageURL,
    onImageChanged,
    smallerSize = false,
    width = smallerSize ? SMALLER_WIDTH : DEFAULT_WIDTH,
    height = smallerSize ? SMALLER_HEIGHT : DEFAULT_HEIGHT,
    innerWidth = width,
    innerHeight = height,
    paddingX = 4,
    paddingY = 4,
    minScale = 0.5,
    maxScale = 1.5,
    defaultScale = Math.max(minScale, Math.min(1.0, maxScale)),
    // Dropzone props
    accept = ACCEPT_FILE_TYPES,
    minSize,
    maxSize,
    disabled,
    debounceAmount = 200,
    buttonContainerClassName,
    sliderContainerClassName,
    uploaderIconClassName,
    uploaderWrapperClassName,
    uploaderTextClassName,
  } = props;

  const [avatarImage, setAvatarImage] = useState<
    string | File | undefined | null
  >(initialImageURL);
  const [avatarEditor, setAvatarEditor] = useState<any>(null);
  const [scale, setScale] = useState(defaultScale);

  const cropFrameWidth = width + paddingX + paddingX;
  const cropFrameHeight = height + paddingY + paddingY;
  const cropWidth = innerWidth;
  const cropHeight = innerHeight;
  const cropOffsetX = 0.5 * (cropFrameWidth - cropWidth);
  const cropOffsetY = 0.5 * (cropFrameHeight - cropHeight);

  // Show crop inner radius only if smaller than the avatar avatar editor border
  const cropInnerRadius =
    cropOffsetX <= paddingX ||
    cropOffsetY <= paddingY ||
    !cropOffsetX ||
    !cropOffsetY
      ? 0
      : 4;

  useEffect(() => {
    setAvatarImage(initialImageURL);
  }, [initialImageURL]);

  const handleDrop = useCallback<NonNullable<DropzoneProps['onDrop']>>(
    (data) => {
      setAvatarImage(data[0]);
    },
    []
  );

  const { getRootProps, getInputProps } = useDropzone({
    onDrop: handleDrop,
    accept,
    minSize,
    maxSize,
    disabled: disabled && !!avatarImage,
  });

  // Debounce updates so we aren't continuously getting image data while dragging
  const updateImageData = useMemo(() => {
    return debounce(() => {
      if (avatarEditor) {
        onImageChanged(avatarEditor.getImage().toDataURL());
      }
    }, debounceAmount);
  }, [avatarEditor, debounceAmount, onImageChanged]);

  return (
    <div className='relative'>
      <div
        className={twMerge(
          'relative overflow-clip rounded-md',
          'aspect-square w-[200px] max-w-full',
          className
        )}
        style={style}
      >
        <div
          {...getRootProps({
            className: twMerge(
              'absolute inset-0 flex flex-column gap-2 cursor-pointer rounded-[inherit]',
              'border-border-primary border text-foreground-primary bg-background-secondary',
              uploaderClassName
            ),
          })}
        >
          <input {...getInputProps()} />
          {avatarImage
            ? null
            : children || (
                <div
                  className={clsx(
                    'absolute inset-0 flex h-full w-full flex-col items-center justify-center gap-1 text-sm',
                    {
                      'gap-1 text-sm': smallerSize,
                      'text-md gap-2': !smallerSize,
                    },
                    uploaderWrapperClassName
                  )}
                >
                  <UploadIcon
                    className={twMerge(
                      smallerSize ? 'h-10 w-8' : 'h-10 w-10',
                      uploaderIconClassName
                    )}
                  />
                  <p className={uploaderTextClassName}>Upload a Photo</p>
                </div>
              )}
        </div>
        {avatarImage && (
          <>
            <div
              className={twMerge(
                'absolute inset-0 h-full w-full',
                innerClassName
              )}
            >
              <AvatarEditor
                className='absolute inset-0 max-h-full min-h-full max-w-full min-w-full object-cover'
                ref={(editor) => setAvatarEditor(editor)}
                width={width}
                height={height}
                image={avatarImage}
                border={[paddingX, paddingY]}
                borderRadius={!paddingX || !paddingY ? 0 : 2}
                color={[0, 0, 0, 0.75]}
                backgroundColor='#000000'
                crossOrigin='anonymous'
                scale={scale}
                onImageReady={() => {
                  updateImageData();
                }}
                onImageChange={() => {
                  if (avatarEditor?.state?.image?.resource) {
                    updateImageData();
                  } else {
                    setScale(defaultScale);
                    onImageChanged(null);
                  }
                }}
              />
              <CropFrame
                className='pointer-events-none absolute inset-0 block h-full w-full text-opacity-black-30'
                width={cropFrameWidth}
                height={cropFrameHeight}
                innerWidth={cropWidth}
                innerHeight={cropHeight}
                innerOffsetX={cropOffsetX}
                innerOffsetY={cropOffsetY}
                innerRadius={cropInnerRadius}
              />
            </div>

            <div
              className={twMerge(
                'absolute inset-x-4 bottom-2 flex flex-col items-center justify-center gap-1 drop-shadow-sm',
                sliderContainerClassName
              )}
            >
              <Slider
                aria-label='Resize Image Slider'
                min={0}
                max={100}
                defaultValue={Math.round(
                  normalizeToScale(defaultScale, minScale, maxScale, 0, 100)
                )}
                onChange={(value) => {
                  setScale(normalizeToScale(value, 0, 100, minScale, maxScale));
                }}
                sx={{
                  '.chakra-slider__track': {
                    bg: 'var(--color-opacity-white-50)',
                  },
                  '.chakra-slider__filled-track': {
                    bg: 'var(--color-opacity-white-90)',
                  },
                  '.chakra-slider__thumb': {
                    bg: 'var(--color-white)',
                  },
                }}
              >
                <SliderTrack>
                  <SliderFilledTrack />
                </SliderTrack>
                <SliderThumb />
              </Slider>
            </div>
          </>
        )}
      </div>
      {avatarImage && (
        <div
          className={twMerge('absolute z-50', buttonContainerClassName)}
          style={{
            position: 'absolute',
            top: '20px',
            left: 'calc(100% - 15px)',
            zIndex: 50,
          }}
        >
          <Button
            className=''
            icon={TrashIcon}
            variant={ButtonVariant.Glass}
            size={ButtonSize.Small}
            shape={ButtonShape.Pill}
            onClick={() => {
              setAvatarImage(null);
              setScale(defaultScale);
              onImageChanged?.(null);
            }}
            aria-label='Remove'
          />
        </div>
      )}
    </div>
  );
};

export default ImageUploader;
