import { PlusCircle as PlusIcon, Upload as UploadIcon } from '@transferwise/icons';
import { clsx } from 'clsx';
import {
  ChangeEvent,
  DragEvent,
  useRef,
  useState,
  forwardRef,
  useImperativeHandle,
  ForwardedRef,
} from 'react';
import { useIntl } from 'react-intl';

import Body from '../../body';
import { FileType, Typography } from '../../common';

import MESSAGES from './UploadButton.messages';
import { DEFAULT_SIZE_LIMIT, imageFileTypes } from './defaults';
import getAllowedFileTypes from './getAllowedFileTypes';

type AllowedFileTypes = string | readonly string[] | readonly FileType[];
export type UploadButtonProps = {
  /**
   * Disable the upload button if your app is not yet ready to accept uploads
   */
  disabled?: boolean;

  /**
   * Should be true, if the UploadInput has at least 1
   * file (valid or invalid) listed.
   */
  withEntries?: boolean;

  /**
   * Allow multiple file uploads
   */
  multiple?: boolean;

  /**
   * List of allowed filetypes, eg. '*' | '.zip,application/zip' | ['.jpg,.jpeg,image/jpeg', '.png,image/png'] (default: image files + PDF)
   */
  fileTypes?: AllowedFileTypes;

  /**
   * Size limit in KBs (1000 KB = 1 MB).
   * If set to `null`, no size limit will be applied.
   * @default 5000
   */
  sizeLimit?: number | null;

  /**
   * Description for the upload button
   */
  description?: string | undefined;

  /**
   * Maximum number of files allowed, if provided, shows error below file item
   */
  maxFiles?: number;

  /**
   * Called when some files were successfully selected
   *
   * @param files
   */
  onChange: (files: FileList) => void;

  /**
   * Id for the upload input
   */
  id?: string;

  /**
   * Title for the upload button
   */
  uploadButtonTitle?: string;
};

export enum TEST_IDS {
  uploadInput = 'uploadInput',
  mediaBody = 'mediaBody',
}

const onDragOver = (event: DragEvent): void => {
  event.preventDefault();
};

const DEFAULT_FILE_INPUT_ID = 'np-upload-button';
const UploadButton = forwardRef<HTMLInputElement | null, UploadButtonProps>(
  (
    {
      disabled,
      withEntries,
      multiple,
      description,
      fileTypes = imageFileTypes,
      sizeLimit = DEFAULT_SIZE_LIMIT,
      maxFiles,
      onChange,
      id = DEFAULT_FILE_INPUT_ID,
      uploadButtonTitle,
    },
    ref: ForwardedRef<HTMLInputElement | null>,
  ) => {
    const { formatMessage } = useIntl();
    const inputRef = useRef<HTMLInputElement | null>(null);

    useImperativeHandle(ref, () => {
      if (!inputRef.current) {
        throw new Error('inputRef.current is null');
      }
      return inputRef.current;
    }, []);

    const [isDropping, setIsDropping] = useState(false);

    const dragCounter = useRef(0);

    const reset = (): void => {
      dragCounter.current = 0;
      setIsDropping(false);
    };

    const onDragLeave = (event: DragEvent): void => {
      event.preventDefault();
      dragCounter.current -= 1;
      if (dragCounter.current === 0) {
        setIsDropping(false);
      }
    };

    const onDragEnter = (event: DragEvent): void => {
      event.preventDefault();
      dragCounter.current += 1;
      if (dragCounter.current === 1) {
        setIsDropping(true);
      }
    };

    const onDrop = (event: DragEvent): void => {
      event.preventDefault();
      reset();
      if (event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
        onChange(event.dataTransfer.files);
      }
    };

    const filesSelected = (event: ChangeEvent<HTMLInputElement>): void => {
      const { files } = event.target;

      if (files) {
        onChange(files);

        if (inputRef.current) {
          inputRef.current.value = '';
        }
      }
    };

    const getFileTypesDescription = (): string => {
      if (fileTypes === '*') {
        return fileTypes;
      }

      return getAllowedFileTypes(Array.isArray(fileTypes) ? fileTypes : [fileTypes]).join(', ');
    };

    function getDescription() {
      if (description) {
        return description;
      }

      const fileTypesDescription = getFileTypesDescription();

      const derivedFileDescription =
        fileTypesDescription === '*' ? formatMessage(MESSAGES.allFileTypes) : fileTypesDescription;

      if (typeof sizeLimit === 'number') {
        return formatMessage(MESSAGES.instructions, {
          fileTypes: derivedFileDescription,
          size: Math.round(sizeLimit / 1000),
        });
      }

      return derivedFileDescription;
    }

    function getAcceptedTypes(): Pick<React.ComponentPropsWithoutRef<'input'>, 'accept'> {
      const areAllFilesAllowed = getFileTypesDescription() === '*';

      if (areAllFilesAllowed) {
        return {}; // file input by default allows all files
      }

      if (Array.isArray(fileTypes)) {
        return { accept: fileTypes.join(',') };
      }

      return { accept: fileTypes as string };
    }

    function renderDescription() {
      return (
        <Body className="np-upload-input__text">
          {getDescription()}
          {maxFiles && (
            <>
              <br />
              {formatMessage(MESSAGES.maximumFiles, { maxFiles })}
            </>
          )}
        </Body>
      );
    }

    function renderButtonTitle() {
      if (uploadButtonTitle) {
        return uploadButtonTitle;
      }
      return formatMessage(multiple ? MESSAGES.uploadFiles : MESSAGES.uploadFile);
    }

    return (
      <label
        className={clsx(
          'np-upload-input__upload-button',
          `np-upload-input__upload-button--${disabled ? 'disabled' : 'enabled'}`,
          `np-upload-input__upload-button--${withEntries ? 'with-entries' : 'without-entries'}`,
          {
            'is-dropping': isDropping,
          },
        )}
        htmlFor={id}
        {...(!disabled && { onDragEnter, onDragLeave, onDrop, onDragOver })}
      >
        <span className="np-upload-input__icon">
          <UploadIcon size={24} className="text-link" />
        </span>
        <div className="np-upload-input__item-content" data-testid={TEST_IDS.mediaBody}>
          <Body type={Typography.BODY_LARGE_BOLD} className="np-upload-input__title">
            {renderButtonTitle()}
          </Body>
          {renderDescription()}
        </div>
        <input
          className="np-upload-input__upload-button-input sr-only"
          type="file"
          id={id}
          {...getAcceptedTypes()}
          {...(multiple && { multiple: true })}
          ref={inputRef}
          disabled={disabled}
          name="file-upload"
          data-testid={TEST_IDS.uploadInput}
          onChange={filesSelected}
        />
        {isDropping && (
          <div
            className={clsx(
              'np-upload-input__drop-file-overlay',
              'droppable-card',
              'droppable-dropping-card',
              'droppable-card-content',
            )}
          >
            <PlusIcon className="m-x-1" size={24} />
            <div>{formatMessage(MESSAGES.dropFile)}</div>
          </div>
        )}
      </label>
    );
  },
);

UploadButton.displayName = 'UploadButton';

export default UploadButton;
