import React, { useState } from "react";
import { useDropzone, FileRejection, FileError } from "react-dropzone";
import { StyledProps } from "../_type";
import { upload, ProgressInfo, UploadEventContext } from "./uploadFile";
import { uuid } from "../_util/uuid";
import { injectValue } from "../_util/inject-value";
import { isCallable } from "../_util/is-callable";
import { File } from "./preset/File";
import { Dragger } from "./preset/Dragger";
import { Image } from "./preset/Image";
import { UploadContext } from "./UploadContext";
import { noop } from "../_util/noop";
import { forwardRefWithStatics } from "../_util/forward-ref-with-statics";

export interface UploadProps extends StyledProps {
  /**
   * 唤起文件选择的组件
   *
   * 直接传递 ReactNode 将默认在该元素使用点击上传
   *
   * 使用 Render Props 形式可自定义点击/拖拽上传
   *
   *  - `open`: 打开文件选择
   *  - `getDraggerProps(props?)`: 在需要执行拖拽的根元素传递这些 props
   *  - `isDragging`: 是否有文件拖拽至可拖拽区域
   *
   * @docType React.ReactNode | (props: { open: () => void, getDraggerProps: (props?: object) => object, isDragging: boolean }) => React.ReactNode;
   */
  children:
    | React.ReactNode
    | ((props: {
        open: () => void;
        getDraggerProps: (props?: object) => object;
        isDragging: boolean;
      }) => React.ReactNode);

  /**
   * 是否支持多选文件
   * @default false
   */
  multiple?: boolean;

  /**
   * 支持上传文件夹
   * @default false
   */
  directory?: boolean;

  /**
   * 上传地址
   */
  action?: string | ((file: File) => Promise<string>);

  /**
   * 接受的文件类型
   */
  accept?: string | string[];

  /**
   * 上传文件大小限制（bytes）
   */
  maxSize?: number;

  /**
   * 请求文件参数名
   * @default "file"
   */
  name?: string;

  /**
   * 设置请求头部
   */
  headers?: object | ((file: File) => object);

  /**
   * 附加请求参数
   */
  data?: object | ((file: File) => object);

  /**
   * 设置请求的 HTTP Method
   * @default "POST"
   * @since 2.5.1
   */
  method?: string;

  /**
   * 自定义 XHR send 发送的数据体
   * @since 2.6.0
   */
  send?: (file: File, formData: FormData) => BodyInit;

  /**
   * 请求时是否携带 cookie
   * @default false
   */
  withCredentials?: boolean;

  /**
   * 上传文件前调用
   *
   *  - `isAccepted`: 当前文件是否符合 `accept` 及 `maxSize` 等限制
   *  - `reasons`: 当前文件不符合要求的原因 （`^2.5.0` 加入）
   *
   * 返回 `true` （Promise `resolve`）执行上传 (`^2.7.4`加入：resolve 传入 File 或 Blob 对象则上传 resolve 传入对象)
   *
   * 返回 `false` （Promise `reject`）时不执行后续上传流程
   */
  beforeUpload?: (
    file: File,
    fileList: File[],
    isAccepted: boolean,
    reasons: FileError[]
  ) => boolean | Promise<any>;

  /**
   * 上传开始时回调
   */
  onStart?: (file: File, context: { xhr: XMLHttpRequest }) => void;

  /**
   * 上传进度更新时回调
   */
  onProgress?: (progress: ProgressInfo, context: UploadEventContext) => void;

  /**
   * 上传成功时回调
   */
  onSuccess?: (result: object | string, context: UploadEventContext) => void;

  /**
   * 上传失败时回调
   */
  onError?: (error: Error, context: UploadEventContext) => void;
}

function isPromise(p: any): p is Promise<any> {
  return p && typeof p === "object" && typeof p.then === "function";
}

export const Upload = forwardRefWithStatics(
  function Upload(
    {
      action = "",
      accept,
      maxSize,
      multiple = false,
      directory = false,
      headers,
      data,
      method,
      send,
      name = "file",
      withCredentials,
      beforeUpload = (_, __, isAccepted) => isAccepted,
      onStart = noop,
      onProgress,
      onSuccess,
      onError,
      children,
      className,
      ...props
    }: UploadProps,
    ref: React.Ref<HTMLSpanElement>
  ) {
    if (directory) {
      // eslint-disable-next-line no-param-reassign
      multiple = true;
    }

    const [dragging, setDragging] = useState(false);

    const { getInputProps, getRootProps, open } = useDropzone({
      accept,
      maxSize,
      multiple,
      noClick: true,
      noKeyboard: true,
      onDrop: uploadFiles,
      onDragEnter: () => setDragging(true),
      onDragLeave: () => setDragging(false),
    });

    function uploadFiles(
      acceptedFiles: File[],
      fileRejections: FileRejection[]
    ) {
      const rejectedFiles = fileRejections.map(r => r.file);
      const fileList = [...acceptedFiles, ...rejectedFiles];

      acceptedFiles.forEach(file => {
        file["id"] = uuid(); // eslint-disable-line dot-notation
        uploadFile(file, fileList, true);
      });

      fileRejections.forEach(({ file, errors }) => {
        file["id"] = uuid(); // eslint-disable-line dot-notation
        uploadFile(file, fileList, false, errors);
      });

      setDragging(false);
    }

    function uploadFile(
      file: File,
      fileList: File[],
      isAccepted: boolean,
      reasons: FileError[] = []
    ) {
      const couldUpload = beforeUpload(file, fileList, isAccepted, reasons);
      if (isPromise(couldUpload)) {
        // support file convert
        couldUpload
          .then(convertedFile =>
            request(
              convertedFile instanceof window.File ||
                convertedFile instanceof window.Blob
                ? convertedFile
                : file
            )
          )
          .catch(() => null);
      } else {
        if (!couldUpload) {
          return;
        }
        setTimeout(() => request(file), 0);
      }
    }

    async function request(file) {
      let uploadAction;
      if (typeof action === "function") {
        try {
          uploadAction = await action(file);
        } catch (error) {
          onError(error, { event: null, xhr: null, file });
          return;
        }
      } else {
        uploadAction = action;
      }

      const xhr = upload({
        action: uploadAction,
        filename: name,
        file,
        data: injectValue(data)(file),
        headers: injectValue(headers)(file),
        method,
        send,
        withCredentials,
        onProgress,
        onSuccess,
        onError,
      });

      onStart(file, { xhr });
    }

    return (
      <UploadContext.Provider
        value={{ open, getDraggerProps: getRootProps, isDragging: dragging }}
      >
        <span ref={ref} className={className} {...props}>
          <input
            {...getInputProps()}
            {...{
              directory: directory ? "directory" : undefined,
              webkitdirectory: directory ? "webkitdirectory" : undefined,
            }}
          />
          {isCallable(children) ? (
            children({
              open,
              getDraggerProps: getRootProps,
              isDragging: dragging,
            })
          ) : (
            <span onClick={open}>{children}</span>
          )}
        </span>
      </UploadContext.Provider>
    );
  },
  {
    defaultLabelAlign: "middle",
    File,
    Dragger,
    Image,
  }
);

Upload.displayName = "Upload";
