import defaultRequest from './request';
import getUid from './uid';
import attrAccept from './attr-accept';
import traverseFileTree from './traverseFileTree';
import type {
  RcFile,
  UploadProgressEvent,
  UploadRequestError,
  BeforeUploadFileType,
} from './interface';
import { uploadProps } from './interface';
import { defineComponent, onBeforeUnmount, onMounted, ref } from 'vue';
import type { ChangeEvent } from '../_util/EventInterface';
import pickAttrs from '../_util/pickAttrs';
import partition from 'lodash-es/partition';

interface ParsedFileInfo {
  origin: RcFile;
  action: string;
  data: Record<string, unknown>;
  parsedFile: RcFile;
}
export default defineComponent({
  name: 'AjaxUploader',
  inheritAttrs: false,
  props: uploadProps(),
  setup(props, { slots, attrs, expose }) {
    const uid = ref(getUid());
    const reqs: any = {};

    const fileInput = ref<HTMLInputElement>();

    let isMounted = false;

    /**
     * Process file before upload. When all the file is ready, we start upload.
     */
    const processFile = async (file: RcFile, fileList: RcFile[]): Promise<ParsedFileInfo> => {
      const { beforeUpload } = props;

      let transformedFile: BeforeUploadFileType | void = file;
      if (beforeUpload) {
        try {
          transformedFile = await beforeUpload(file, fileList);
        } catch (e) {
          // Rejection will also trade as false
          transformedFile = false;
        }
        if (transformedFile === false) {
          return {
            origin: file,
            parsedFile: null,
            action: null,
            data: null,
          };
        }
      }

      // Get latest action
      const { action } = props;
      let mergedAction: string;
      if (typeof action === 'function') {
        mergedAction = await action(file);
      } else {
        mergedAction = action;
      }

      // Get latest data
      const { data } = props;
      let mergedData: Record<string, unknown>;
      if (typeof data === 'function') {
        mergedData = await data(file);
      } else {
        mergedData = data;
      }

      const parsedData =
        // string type is from legacy `transformFile`.
        // Not sure if this will work since no related test case works with it
        (typeof transformedFile === 'object' || typeof transformedFile === 'string') &&
        transformedFile
          ? transformedFile
          : file;

      let parsedFile: File;
      if (parsedData instanceof File) {
        parsedFile = parsedData;
      } else {
        parsedFile = new File([parsedData], file.name, { type: file.type });
      }

      const mergedParsedFile: RcFile = parsedFile as RcFile;
      mergedParsedFile.uid = file.uid;

      return {
        origin: file,
        data: mergedData,
        parsedFile: mergedParsedFile,
        action: mergedAction,
      };
    };

    const post = ({ data, origin, action, parsedFile }: ParsedFileInfo) => {
      if (!isMounted) {
        return;
      }

      const { onStart, customRequest, name, headers, withCredentials, method } = props;

      const { uid } = origin;
      const request = customRequest || defaultRequest;

      const requestOption = {
        action,
        filename: name,
        data,
        file: parsedFile,
        headers,
        withCredentials,
        method: method || 'post',
        onProgress: (e: UploadProgressEvent) => {
          const { onProgress } = props;
          onProgress?.(e, parsedFile);
        },
        onSuccess: (ret: any, xhr: XMLHttpRequest) => {
          const { onSuccess } = props;
          onSuccess?.(ret, parsedFile, xhr);

          delete reqs[uid];
        },
        onError: (err: UploadRequestError, ret: any) => {
          const { onError } = props;
          onError?.(err, ret, parsedFile);

          delete reqs[uid];
        },
      };

      onStart(origin);
      reqs[uid] = request(requestOption);
    };

    const reset = () => {
      uid.value = getUid();
    };

    const abort = (file?: any) => {
      if (file) {
        const uid = file.uid ? file.uid : file;
        if (reqs[uid] && reqs[uid].abort) {
          reqs[uid].abort();
        }
        delete reqs[uid];
      } else {
        Object.keys(reqs).forEach(uid => {
          if (reqs[uid] && reqs[uid].abort) {
            reqs[uid].abort();
          }
          delete reqs[uid];
        });
      }
    };

    onMounted(() => {
      isMounted = true;
    });

    onBeforeUnmount(() => {
      isMounted = false;
      abort();
    });
    const uploadFiles = (files: File[]) => {
      const originFiles = [...files] as RcFile[];
      const postFiles = originFiles.map((file: RcFile & { uid?: string }) => {
        // eslint-disable-next-line no-param-reassign
        file.uid = getUid();
        return processFile(file, originFiles);
      });

      // Batch upload files
      Promise.all(postFiles).then(fileList => {
        const { onBatchStart } = props;

        onBatchStart?.(fileList.map(({ origin, parsedFile }) => ({ file: origin, parsedFile })));

        fileList
          .filter(file => file.parsedFile !== null)
          .forEach(file => {
            post(file);
          });
      });
    };

    const onChange = (e: ChangeEvent) => {
      const { accept, directory } = props;
      const { files } = e.target as any;
      const acceptedFiles = [...files].filter(
        (file: RcFile) => !directory || attrAccept(file, accept),
      );
      uploadFiles(acceptedFiles);
      reset();
    };

    const onClick = (e: MouseEvent | KeyboardEvent) => {
      const el = fileInput.value;
      if (!el) {
        return;
      }
      const { onClick } = props;
      // TODO
      // if (children && (children as any).type === 'button') {
      //   const parent = el.parentNode as HTMLInputElement;
      //   parent.focus();
      //   parent.querySelector('button').blur();
      // }
      el.click();
      if (onClick) {
        onClick(e);
      }
    };

    const onKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'Enter') {
        onClick(e);
      }
    };

    const onFileDrop = (e: DragEvent) => {
      const { multiple } = props;

      e.preventDefault();

      if (e.type === 'dragover') {
        return;
      }

      if (props.directory) {
        traverseFileTree(
          Array.prototype.slice.call(e.dataTransfer.items),
          uploadFiles,
          (_file: RcFile) => attrAccept(_file, props.accept),
        );
      } else {
        const files: [RcFile[], RcFile[]] = partition(
          Array.prototype.slice.call(e.dataTransfer.files),
          (file: RcFile) => attrAccept(file, props.accept),
        );
        let successFiles = files[0];
        const errorFiles = files[1];
        if (multiple === false) {
          successFiles = successFiles.slice(0, 1);
        }

        uploadFiles(successFiles);
        if (errorFiles.length && props.onReject) props.onReject(errorFiles);
      }
    };
    expose({
      abort,
    });
    return () => {
      const {
        componentTag: Tag,
        prefixCls,
        disabled,
        id,
        multiple,
        accept,
        capture,
        directory,
        openFileDialogOnClick,
        onMouseenter,
        onMouseleave,
        ...otherProps
      } = props;
      const cls = {
        [prefixCls]: true,
        [`${prefixCls}-disabled`]: disabled,
        [attrs.class as string]: !!attrs.class,
      };
      // because input don't have directory/webkitdirectory type declaration
      const dirProps: any = directory
        ? { directory: 'directory', webkitdirectory: 'webkitdirectory' }
        : {};
      const events = disabled
        ? {}
        : {
            onClick: openFileDialogOnClick ? onClick : () => {},
            onKeydown: openFileDialogOnClick ? onKeyDown : () => {},
            onMouseenter,
            onMouseleave,
            onDrop: onFileDrop,
            onDragover: onFileDrop,
            tabindex: '0',
          };
      return (
        <Tag {...events} class={cls} role="button" style={attrs.style}>
          <input
            {...pickAttrs(otherProps, { aria: true, data: true })}
            id={id}
            type="file"
            ref={fileInput}
            onClick={e => e.stopPropagation()} // https://github.com/ant-design/ant-design/issues/19948
            key={uid.value}
            style={{ display: 'none' }}
            accept={accept}
            {...dirProps}
            multiple={multiple}
            onChange={onChange}
            {...(capture != null ? { capture } : {})}
          />
          {slots.default?.()}
        </Tag>
      );
    };
  },
});