import React, { ReactElement, useRef, useCallback } from 'react';
import css from '../../../utils/css';

import Icon from '../../Icon';
import Typography from '../../Typography';
import { DropzoneWrapper, HiddenInput } from './StyledDragAndDrop';
import { isValidExtension } from './utils';

import { CommonProps } from '../../common';
import { fromUndefinedable, getOrElse, map } from '../../../fp/Option';
import { invoke, invokeWith, noop, pipe } from '../../../fp/function';
import { reduce } from '../../../fp/Array';
import { Either, match, right, left } from '../../../fp/Either';

export interface DragAndDropProps extends CommonProps {
  /**
   * A comma-separated list of one or more file types, or [unique file type specifiers](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept#unique_file_type_specifiers), describing which file types to allow.
   */
  accept?: string;
  /**
   * Description for accepted files.
   */
  description?: string | ReactElement;
  /**
   * Maximum file size (in `bytes`).
   */
  maxSize?: number;
  /**
   * Allow to drag and drop/select multiple files.
   */
  multiple?: boolean;
  /**
   * Event handler receiving accepted files.
   */
  onAccept?: (files: File[]) => void;
  /**
   * Event handler receiving rejected files.
   */
  onReject?: (
    rejectedFiles: {
      file: File;
      reason: 'size-limit-exceeded' | 'format-not-allowed';
    }[]
  ) => void;
  /**
   * Displayed text.
   */
  text?: string | ReactElement;
}

const DEFAULT_MAX_SIZE = 100 * 1024 * 1024 * 1024; // 100GBs

const preventDefaultEvent = (e: React.MouseEvent<HTMLElement>): void => {
  e.preventDefault();
};

const stopPropagationEvent = (e: React.MouseEvent<HTMLElement>): void => {
  e.stopPropagation();
};

const eitherValidOrInvalid = (
  allowedExtensions: string[],
  allowedMaxSize: number
) => (
  file: File
): Either<File, [File, 'format-not-allowed' | 'size-limit-exceeded']> => {
  const hasValidExtension = allowedExtensions.some(
    isValidExtension(file.name, file.type)
  );
  if (allowedExtensions.length > 0 && hasValidExtension === false) {
    return right([file, 'format-not-allowed']);
  }
  if (file.size > allowedMaxSize) {
    return right([file, 'size-limit-exceeded']);
  }
  return left(file);
};

const DragAndDrop = ({
  text,
  description,
  onAccept,
  onReject,
  multiple = false,
  accept,
  maxSize,
  id,
  className,
  style,
  sx = {},
  'data-test-id': dataTestId,
}: DragAndDropProps): ReactElement => {
  const inputRef = useRef<HTMLInputElement>(null);
  const onDropzoneClick = useCallback(() => {
    if (inputRef.current !== null) {
      inputRef.current.value = '';
      inputRef.current.click();
    }
  }, []);

  const allowedExtensions = React.useMemo(
    () =>
      pipe(
        accept,
        fromUndefinedable,
        map(acc => acc.split(',').map(i => i.trim())),
        getOrElse(() => [])
      ),
    [accept]
  );

  const allowedMaxSize = React.useMemo(
    () =>
      pipe(
        maxSize,
        fromUndefinedable,
        getOrElse(() => DEFAULT_MAX_SIZE)
      ),
    [maxSize]
  );

  const onChanged = React.useCallback(
    (files: File[]) => {
      const { validFiles, invalidFiles } = pipe(
        multiple === true ? files : files.slice(0, 1),
        reduce(
          (
            acc: {
              invalidFiles: {
                file: File;
                reason: 'format-not-allowed' | 'size-limit-exceeded';
              }[];
              validFiles: File[];
            },
            cur: File
          ) =>
            pipe(
              cur,
              eitherValidOrInvalid(allowedExtensions, allowedMaxSize),
              match(
                file => ({ ...acc, validFiles: [...acc.validFiles, file] }),
                ([file, reason]) => ({
                  ...acc,
                  invalidFiles: [...acc.invalidFiles, { file, reason }],
                })
              )
            ),
          { validFiles: [], invalidFiles: [] }
        )
      );
      if (validFiles.length > 0) {
        pipe(
          onAccept,
          fromUndefinedable,
          getOrElse(() => noop),
          invokeWith(validFiles)
        );
      }

      if (invalidFiles.length > 0) {
        pipe(
          onReject,
          fromUndefinedable,
          getOrElse(() => noop),
          invokeWith(invalidFiles)
        );
      }
    },
    [multiple, allowedMaxSize, allowedExtensions, onAccept, onReject]
  );
  const onDrop = useCallback(
    e => {
      preventDefaultEvent(e);
      pipe(e.dataTransfer.files, Array.from, invoke(onChanged));
    },
    [onChanged]
  );
  const onInputChange = useCallback(
    e => {
      pipe(e.target.files, Array.from, invoke(onChanged));
    },
    [onChanged]
  );

  return (
    <DropzoneWrapper
      onClick={onDropzoneClick}
      onDragOver={preventDefaultEvent}
      onDragEnter={preventDefaultEvent}
      onDragLeave={preventDefaultEvent}
      onDrop={onDrop}
      className={className}
      style={{ ...style, ...css(sx) }}
      data-test-id={dataTestId}
    >
      <div>
        {text !== undefined && (
          <div>
            <Icon icon="upload-outlined" size="xlarge" intent="primary" />
            <Typography.Text>{text}</Typography.Text>
          </div>
        )}
        {description !== undefined && (
          <Typography.Text intent="subdued">{description}</Typography.Text>
        )}
      </div>
      <HiddenInput
        type="file"
        ref={inputRef}
        onChange={onInputChange}
        multiple={multiple}
        accept={accept}
        onClick={stopPropagationEvent}
        id={id}
      />
    </DropzoneWrapper>
  );
};

export default DragAndDrop;
