import classNames from "classnames";
import React, {
  HTMLProps,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import { DndProvider, useDrop, DropTargetMonitor } from "react-dnd";
import { HTML5Backend, NativeTypes } from "react-dnd-html5-backend";
import mime from "mime-types";

import { bemHOF } from "../../utilities";
import { Button, BUTTON_VARIANT } from "../Button";
import { FormGroupContext } from "../FormGroup/FormGroupContext";
import { Text } from "../Text";
import { Truncate } from "../Truncate";

const cn = bemHOF("FileUpload");
const ERROR_TIMEOUT = 5000;

interface FileUploadDndPropsBase
  extends Omit<HTMLProps<HTMLLabelElement>, "onChange" | "value"> {
  /**
   * Classes passed to the wrapping `<label />` element
   */
  className?: string;

  /**
   * Comma-separated string that defines what types of files will be accepted.
   * See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#attr-accept
   */
  accept?: string;

  /**
   * Replaces the default button text
   */
  label?: string;

  /**
   * Props passed directly to the underlying `<input />` element. Note that the `value` and `files`
   * values on this `<input />` may not be accurate since the drag and drop functionality operates
   * separately from it.
   */
  inputProps?: HTMLProps<HTMLInputElement>;
}

interface FileUploadDndPropsSingle extends FileUploadDndPropsBase {
  /**
   * When the `multiple` prop is not set to `true`, `onChange` will be called with the selected file or `null`.
   */
  onChange: (file: File | null) => void;
  /**
   * When the `multiple` prop is not set to `true`, `value` will be the selected file or `null`.
   */
  value: File | null;
  /**
   * Sets whether or not the user is able to upload multiple files at a time
   */
  multiple?: false;
}

interface FileUploadDndPropsMultiple extends FileUploadDndPropsBase {
  /**
   * When the `multiple` prop is set to `true`, `onChange` will be called with an array of selected files or `null`.
   */
  onChange: (files: File[] | null) => void;
  /**
   * When the `multiple` prop is set to `true`, `value` will be an array of selected files or `null`.
   */
  value: File[] | null;
  multiple: true;
}

export type FileUploadDndProps =
  | FileUploadDndPropsSingle
  | FileUploadDndPropsMultiple;

const FileUploadInput = ({
  inputProps,
  className,
  accept,
  label = "Select file",
  ...props
}: FileUploadDndProps) => {
  const inputRef = useRef<HTMLInputElement>(null);

  const { id: contextId } = useContext(FormGroupContext);
  const inputId = inputProps ? inputProps.id : undefined;
  const id = inputId || contextId;

  const [errorText, setErrorText] = useState("");

  const acceptArray = useMemo(() => (accept ? accept.split(",") : null), [
    accept,
  ]);

  const validateFiles = useCallback(
    (files: File[]) => {
      const disallowedTypes = new Set<string>();

      if (acceptArray) {
        files.forEach((f) => {
          let fileType = f.type;
          if (fileType === "") {
            const split_name = f.name.split(".");
            const type = split_name[split_name.length - 1];
            fileType = mime.lookup(type) || "unknown";
          }

          const allowed = acceptArray.includes(fileType);
          if (!allowed) {
            disallowedTypes.add(fileType);
          }
        });
        if (disallowedTypes.size > 0) {
          setErrorText(
            `File type${disallowedTypes.size > 1 ? "s" : ""} not allowed: ${[
              ...disallowedTypes,
            ].join(", ")}`,
          );
          return false;
        }
      }

      return true;
    },
    [acceptArray],
  );

  const handleChange = useCallback(
    (files: File[] | null) => {
      if (!files) {
        props.onChange(null);
        return;
      }

      if (props.multiple) {
        props.onChange(files);
      } else {
        props.onChange(files[0]);
      }
    },
    [props],
  );

  const handleFileDrop = useCallback(
    (item: { files: File[] }) => {
      if (item) {
        const { files } = item;
        const isValid = validateFiles(files);

        if (!isValid) {
          handleChange(null);
          return;
        }

        if (props.multiple) {
          handleChange(files);
        } else {
          handleChange([files[0]]);
        }
      }
    },
    [handleChange, props.multiple, validateFiles],
  );

  const [{ canDrop, isOver }, drop] = useDrop(
    () => ({
      accept: [NativeTypes.FILE],
      drop(item: { files: File[] }) {
        handleFileDrop(item);
      },
      collect: (monitor: DropTargetMonitor) => ({
        isOver: monitor.isOver(),
        canDrop: monitor.canDrop(),
      }),
    }),
    [handleFileDrop],
  );
  const isActive = canDrop && isOver;

  const handleInputChange = () => {
    const inputElement = inputRef.current;
    const files = inputElement && inputElement.files;
    const filesArray = files ? Array.from(files) : [];
    const isValid = validateFiles(filesArray);
    handleChange(isValid ? filesArray : null);
  };

  const { className: inputClassName, ...inputRest } = inputProps || {};
  const { value, onChange, multiple, ...labelRest } = props;

  useEffect(() => {
    const errorTimer = setTimeout(() => {
      if (errorText) {
        setErrorText("");
      }
    }, ERROR_TIMEOUT);
    return () => clearTimeout(errorTimer);
  }, [errorText]);

  const fileLabel = useMemo(() => {
    const emptyText = multiple ? "No files chosen" : "No file chosen";
    if (!value) {
      return emptyText;
    }

    if (Array.isArray(value)) {
      if (value.length === 0) return emptyText;
      return `${value.length} files selected`;
    }

    return value.name;
  }, [value, multiple]);

  return (
    <label
      ref={drop}
      htmlFor={id}
      className={classNames(cn(), isActive && cn({ m: "active" }), className)}
      {...labelRest}
    >
      <input
        className={classNames(cn({ e: "input" }), inputClassName)}
        ref={inputRef}
        type="file"
        id={id}
        multiple={props.multiple}
        accept={accept}
        {...inputRest}
        onChange={handleInputChange}
      />
      <Button
        className={classNames(cn({ e: "button" }))}
        as="div"
        variant={BUTTON_VARIANT.SECONDARY}
      >
        {label}
      </Button>
      <Truncate className={cn({ e: "truncate" })}>
        {errorText ? (
          <Text as="span" className={cn({ e: "error" })}>
            {errorText}
          </Text>
        ) : (
          <Text className={classNames(cn({ e: "fileLabel" }))}>
            {fileLabel}
          </Text>
        )}
      </Truncate>
    </label>
  );
};

export const FileUploadDnD = (props: FileUploadDndProps) => {
  return (
    <DndProvider backend={HTML5Backend}>
      <FileUploadInput {...props} />
    </DndProvider>
  );
};
