import React, { Suspense } from 'react';
import { FormItem, FormControlProps, FormBaseControl } from './Item';
// import 'cropperjs/dist/cropper.css';
const Cropper = React.lazy(() => import('react-cropper'));
import DropZone from 'react-dropzone';
import { FileRejection } from 'react-dropzone';
import 'blueimp-canvastoblob';
import find from 'lodash/find';
import { Payload } from '../../types';
import { buildApi } from '../../utils/api';
import {
  createObject,
  qsstringify,
  guid,
  isEmpty,
  qsparse,
  isMobile
} from '../../utils/helper';
import { Icon } from '../../components/icons';
import Button from '../../components/Button';
import accepts from 'attr-accept';
import { getNameFromUrl } from './InputFile';
import ImageComponent, { ImageThumbProps } from '../Image';
import { TranslateFn } from '../../locale';
import { dataMapping } from '../../utils/tpl-builtin';
import {
  SchemaApi,
  SchemaClassName,
  SchemaTokenizeableString,
  SchemaUrlPath
} from '../../Schema';
import { filter } from '../../utils/tpl';
import isPlainObject from 'lodash/isPlainObject';
import merge from 'lodash/merge';
import Popover from 'antd/lib/popover';
import Input from 'antd/lib/input';
import { message } from 'antd';
import SparkMD5 from 'spark-md5';
import { Shell } from '../../utils/shell';

/**
 * Image 图片上传控件
 * 文档：https://baidu.gitee.io/amis/docs/components/form/image
 * 该组件已弃用，统一使用lion-upload
 */
export interface ImageControlSchema extends FormBaseControl {
  /**
   * 指定为图片上传控件
   */
  type: 'input-image';

  /**
   * 默认展示图片的链接
   */
  src?: SchemaUrlPath;

  /**
   * 默认展示图片的类名
   */
  imageClassName?: string;

  /**
   * 配置接收的图片类型
   *
   * 建议直接填写文件后缀
   * 如：.txt,.csv
   *
   * 多个类型用逗号隔开。
   */
  accept?: string;

  /**
   * 默认都是通过用户选择图片后上传返回图片地址，如果开启此选项，则可以允许用户图片地址。
   */
  allowInput?: boolean;

  /**
   * 是否自动开始上传
   */
  autoUpload?: boolean;

  /**
   * 选择图片按钮的 CSS 类名
   */
  btnClassName?: SchemaClassName;

  /**
   * 上传按钮的 CSS 类名
   */
  btnUploadClassName?: SchemaClassName;

  /**
   * @deprecated
   */
  compress?: boolean;

  /**
   * @deprecated
   */
  compressOptions?: {
    maxHeight?: number;
    maxWidth?: number;
  };

  crop?:
  | boolean
  | {
    /**
     * 默认 `1` 即 `1:1`
     *
     * 留空将不限制
     */
    aspectRatio?: number;

    guides?: boolean;
    dragMode?: string;
    viewMode?: number;
    rotatable?: boolean;
    scalable?: boolean;
  };

  /**
   * 裁剪后的图片类型
   */
  cropFormat?: string;

  /**
   * 裁剪后的质量
   */
  cropQuality?: number;

  /**
   * 是否允许二次裁剪。
   */
  reCropable?: boolean;

  /**
   * 是否隐藏上传按钮
   */
  hideUploadButton?: boolean;

  /**
   * 限制图片大小，超出不让上传。
   */
  limit?: {
    /**
     * 比率不对时的提示文字
     */
    aspectRatioLabel?: string;
    /**
     * 限制比率
     */
    aspectRatio?: number;

    /**
     * 限制图片高度
     */
    height?: number;

    /**
     *  限制图片宽度
     */
    width?: number;

    /**
     * 限制图片最大高度
     */
    maxHeight?: number;

    /**
     * 限制图片最大宽度
     */
    maxWidth?: number;

    /**
     * 限制图片最小高度
     */
    minHeight?: number;

    /**
     *  限制图片最小宽度
     */
    minWidth?: number;
  };

  /**
   * 最多的个数
   */
  maxLength?: number;

  /**
   * 默认没有限制，当设置后，文件大小大于此值将不允许上传。
   */
  maxSize?: number;

  /**
   * 默认 `/api/upload` 如果想自己存储，请设置此选项。
   */
  receiver?: SchemaApi;

  /**
   * 默认为 false, 开启后，允许用户输入压缩选项。
   *
   * @deprecated
   */
  showCompressOptions?: boolean;

  /**
   * 是否为多选
   */
  multiple?: boolean;

  /**
   * 单选模式：当用户选中某个选项时，选项中的 value 将被作为该表单项的值提交，否则，整个选项对象都会作为该表单项的值提交。
   * 多选模式：选中的多个选项的 `value` 会通过 `delimiter` 连接起来，否则直接将以数组的形式提交值。
   */
  joinValues?: boolean;

  /**
   * 分割符
   */
  delimiter?: string;

  /**
   * 开启后将选中的选项 value 的值封装为数组，作为当前表单项的值。
   */
  extractValue?: boolean;

  /**
   * 清除时设置的值
   */
  resetValue?: any;

  /**
   * 缩路图展示模式
   */
  thumbMode?: 'w-full' | 'h-full' | 'contain' | 'cover';

  /**
   * 缩路图展示比率。
   */
  thumbRatio?: '1:1' | '4:3' | '16:9';

  /**
   * 上传后把其他字段同步到表单内部。
   */
  autoFill?: {
    [propName: string]: SchemaTokenizeableString;
  };

  /**
   * 默认占位图图片地址
   */
  frameImage?: SchemaUrlPath;

  /**
   * 是否开启固定尺寸
   */
  fixedSize?: boolean;

  /**
   * 固定尺寸的 CSS类名
   */
  fixedSizeClassName?: SchemaClassName;

  /**
   * 上传url
   */
  isUrl: boolean;
}

let preventEvent = (e: any) => e.stopPropagation();

export interface ImageProps
  extends FormControlProps,
  Omit<
  ImageControlSchema,
  'type' | 'className' | 'descriptionClassName' | 'inputClassName'
  > {
  onImageEnlarge?: (
    info: Pick<ImageThumbProps, 'src' | 'originalSrc' | 'title' | 'caption'> & {
      index?: number;
      list?: Array<
        Pick<ImageThumbProps, 'src' | 'originalSrc' | 'title' | 'caption'>
      >;
    }
  ) => void;
}

export interface ImageState {
  uploading: boolean;
  locked: boolean;
  lockedReason?: string;
  files: Array<FileValue | FileX>;
  crop?: any;
  error?: string;
  cropFile?: FileValue;
  cropFileName?: string; // 主要是用于后续上传的时候获得用户名
  submitOnChange?: boolean;
  frameImageWidth?: number;
  pictureUrl: string;
  uploadParams: any;
  arrayBufferData: any;
  chunksSize: number,   // 上传文件分块的总个数
  upload_cmd: string,
  upload_name: string,
  file_type: string,
  fileSize: number
}

export interface FileValue {
  value?: any;
  state: 'init' | 'error' | 'pending' | 'uploading' | 'uploaded' | 'invalid';
  url?: string;
  error?: string;
  info?: {
    width: number;
    height: number;
    len?: number;
  };
  [propName: string]: any;
}

export interface FileX extends File {
  id?: string | number;
  preview?: string;
  state?: 'init' | 'error' | 'pending' | 'uploading' | 'uploaded' | 'invalid';
  progress?: number;
  [propName: string]: any;
}

export default class ImageControl extends React.Component<
  ImageProps,
  ImageState
> {
  static defaultProps = {
    limit: undefined,
    accept: 'image/jpeg, image/jpg, image/png, image/gif',
    receiver: '/api/upload',
    hideUploadButton: false,
    placeholder: 'Image.placeholder',
    joinValues: true,
    extractValue: false,
    delimiter: ',',
    autoUpload: true,
    multiple: false
  };

  static formatFileSize(
    size: number | string,
    units = [' B', ' KB', ' M', ' G']
  ) {
    size = parseInt(size as string, 10) || 0;

    while (size > 1024 && units.length > 1) {
      size /= 1024;
      units.shift();
    }

    return size.toFixed(2) + units[0];
  }

  static valueToFile(
    value: string | object,
    props?: ImageProps
  ): FileValue | undefined {
    return value
      ? {
        ...(typeof value === 'string'
          ? {
            value,
            url: value,
            id: guid()
          }
          : value),
        state: 'init'
      }
      : undefined;
  }

  static sizeInfo(
    width: number | undefined,
    height: number | undefined,
    __: TranslateFn
  ): string {
    if (!width) {
      return __('Image.height', { height: height });
    } else if (!height) {
      return __('Image.width', { width: width });
    }

    return __('Image.size', { width, height });
  }

  state: ImageState = {
    uploading: false,
    locked: false,
    files: [],
    pictureUrl: '',
    uploadParams: {},
    arrayBufferData: [],
    chunksSize: 0,   // 上传文件分块的总个数
    upload_cmd: 'check_exist',
    upload_name: '',
    file_type: '',
    fileSize: 0
  };

  files: Array<FileValue | FileX> = [];
  fileUploadCancelExecutors: Array<{
    file: any;
    executor: () => void;
  }> = [];
  cropper: Cropper;
  dropzone = React.createRef<any>();
  frameImageRef = React.createRef<any>();
  current: FileValue | FileX | null = null;
  resolve?: (value?: any) => void;
  emitValue: any;
  unmounted = false;

  constructor(props: ImageProps) {
    super(props);
    const value: string | Array<string | FileValue> | FileValue = props.value;
    const multiple = props.multiple;
    const joinValues = props.joinValues;
    const delimiter = props.delimiter as string;
    let files: Array<FileValue> = [];

    if (value) {
      // files = (multiple && Array.isArray(value) ? value : joinValues ? (value as string).split(delimiter) : [value])
      files = (
        Array.isArray(value)
          ? value
          : joinValues && typeof value === 'string' && multiple
            ? (value as string).split(delimiter)
            : [value]
      )
        .map(item => ImageControl.valueToFile(item) as FileValue)
        .filter(item => item);
    }

    this.state = {
      ...this.state,
      files: (this.files = files),
      crop: this.buildCrop(props),
      frameImageWidth: 0
    };

    this.sendFile = this.sendFile.bind(this);
    this.removeFile = this.removeFile.bind(this);
    this.handleDrop = this.handleDrop.bind(this);
    this.handleClick = this.handleClick.bind(this);
    this.handleClick = this.handleClick.bind(this);
    this.handleCrop = this.handleCrop.bind(this);
    this.handleDropRejected = this.handleDropRejected.bind(this);
    this.cancelCrop = this.cancelCrop.bind(this);
    this.rotatableCrop = this.rotatableCrop.bind(this);
    this.handleImageLoaded = this.handleImageLoaded.bind(this);
    this.handleFrameImageLoaded = this.handleFrameImageLoaded.bind(this);
    this.startUpload = this.startUpload.bind(this);
    this.stopUpload = this.stopUpload.bind(this);
    this.toggleUpload = this.toggleUpload.bind(this);
    this.tick = this.tick.bind(this);
    this.onChange = this.onChange.bind(this);
    this.addFiles = this.addFiles.bind(this);
    this.handleSelect = this.handleSelect.bind(this);
    this.handlePaste = this.handlePaste.bind(this);
    this.syncAutoFill = this.syncAutoFill.bind(this);
  }

  componentDidMount() {
    this.syncAutoFill();
  }

  componentDidUpdate(prevProps: ImageProps) {
    const props = this.props;

    if (prevProps.value !== props.value && this.emitValue !== props.value) {
      const value: string | Array<string | FileValue> | FileValue = props.value;
      const multiple = props.multiple;
      const joinValues = props.joinValues;
      const delimiter = props.delimiter as string;

      let files: Array<FileValue> = [];

      if (value) {
        files = (
          Array.isArray(value)
            ? value
            : joinValues && typeof value === 'string' && multiple
              ? (value as string).split(delimiter)
              : [value]
        )
          .map(item => {
            let obj = ImageControl.valueToFile(item, props) as FileValue;
            let org;

            if (
              obj &&
              (org = find(
                this.files,
                item => (item as FileValue).value === obj.value
              ))
            ) {
              obj = {
                ...org,
                ...obj,
                id: org.id || obj.id
              };
            }

            return obj;
          })
          .filter(item => item);
      }

      this.setState(
        {
          files: (this.files = files)
        },
        this.syncAutoFill
      );
    }

    if (prevProps.crop !== props.crop) {
      this.setState({
        crop: this.buildCrop(props)
      });
    }
  }

  componentWillUnmount() {
    this.unmounted = true;
  }

  buildCrop(props: ImageProps) {
    let crop = props.crop;
    const __ = this.props.translate;

    if (crop && props.multiple) {
      props.env && props.env.alert && props.env.alert(__('Image.configError'));
      return null;
    }

    if (crop === true) {
      crop = {};
    }

    if (crop) {
      crop = {
        aspectRatio: undefined, // 默认不限制
        guides: true,
        dragMode: 'move',
        viewMode: 1,
        rotatable: true,
        scalable: true,
        ...crop
      };
    }

    return crop;
  }

  handleDropRejected(
    rejectedFiles: FileRejection[],
    evt: React.DragEvent<any>
  ) {
    if (evt.type !== 'change' && evt.type !== 'drop') {
      return;
    }
    const { multiple, env, accept, translate: __ } = this.props;

    const files = rejectedFiles.map(fileRejection => ({
      ...fileRejection.file,
      state: 'invalid',
      id: guid(),
      name: fileRejection.file.name
    }));

    // this.setState({
    //   files: this.files = multiple
    //     ? this.files.concat(files)
    //     : this.files.length
    //     ? this.files
    //     : files.slice(0, 1)
    // });

    env.alert(
      __('File.invalidType', {
        files: files.map((file: any) => `「${file.name}」`).join(' '),
        accept
      })
    );
  }

  startUpload(retry: boolean = false) {
    if (this.state.uploading) {
      return;
    }

    this.setState(
      {
        uploading: true,
        locked: true,
        files: (this.files = this.files.map(file => {
          if (retry && file.state === 'error') {
            file.state = 'pending';
            file.progress = 0;
          }

          return file;
        }))
      },
      this.tick
    );
  }

  toggleUpload() {
    return this.state.uploading ? this.stopUpload() : this.startUpload();
  }

  stopUpload() {
    if (!this.state.uploading) {
      return;
    }

    this.setState({
      uploading: false
    });
  }

  tick() {
    if (this.current || !this.state.uploading) {
      return;
    }

    const env = this.props.env;
    const __ = this.props.translate;
    const file = find(this.files, item => item.state === 'pending') as FileX;
    if (file) {
      this.current = file;

      file.state = 'uploading';
      this.setState(
        {
          files: (this.files = this.files.concat())
        },
        () =>
          this.sendFile(
            file as FileX,
            (error, file, obj) => {
              const files = this.files.concat();
              const idx = files.indexOf(file);

              if (!~idx) {
                return;
              }

              let newFile: FileX | FileValue = file;

              if (error) {
                newFile.state =
                  file.state !== 'uploading' ? file.state : 'error';
                newFile.error = error;

                if (!this.props.multiple && newFile.state === 'invalid') {
                  files.splice(idx, 1);
                  this.current = null;

                  return this.setState(
                    {
                      files: (this.files = files),
                      error: error
                    },
                    this.tick
                  );
                }

                env.notify('error', error || __('File.errorRetry'));
              } else {
                newFile = {
                  name: file.name || this.state.cropFileName,
                  ...obj,
                  preview: file.preview
                } as FileValue;
              }
              files.splice(idx, 1, newFile);
              this.current = null;
              this.setState(
                {
                  files: (this.files = files)
                },
                this.tick
              );
            },
            progress => {
              const files = this.files.concat();
              const idx = files.indexOf(file);

              if (!~idx) {
                return;
              }

              // file 是个非 File 对象，先不copy了直接改。
              file.progress = progress;
              this.setState({
                files: (this.files = files)
              });
            }
          )
      );
    } else {
      this.setState(
        {
          uploading: false,
          locked: false
        },
        () => {
          this.onChange(!!this.resolve);

          if (this.resolve) {
            this.resolve(
              this.files.some(file => file.state === 'error')
                ? __('File.errorRetry')
                : null
            );
            this.resolve = undefined;
          }
        }
      );
    }
  }

  removeFile(file: FileValue, index: number) {
    const files = this.files.concat();

    this.removeFileCanelExecutor(file, true);
    files.splice(index, 1);

    const isUploading = this.current === file;
    if (isUploading) {
      this.current = null;
    }

    this.setState(
      {
        files: (this.files = files)
      },
      isUploading ? this.tick : this.onChange
    );
  }

  previewImage(file: FileX, index: number, e: React.MouseEvent<any>) {
    const { onImageEnlarge } = this.props;

    if (onImageEnlarge) {
      const files = this.files;
      e.preventDefault();

      onImageEnlarge({
        src: (file.preview || file.url || file) as string,
        originalSrc: (file.preview || file.url || file) as string,
        index,
        list: files.map(file => ({
          src: (file.preview || file.url || file) as string,
          originalSrc: (file.preview || file.url || file) as string,
          title: file.name || getNameFromUrl(file.value || file.url || file)
        }))
      });
    }
  }

  editImage(index: number) {
    const files = this.files;

    this.setState({
      cropFile: {
        preview: files[index].preview || (files[index].url as string),
        name: files[index].name,
        state: 'init'
      },
      cropFileName: files[index].name
    });
  }

  onChange(changeImmediately?: boolean) {
    const {
      multiple,
      onChange,
      joinValues,
      extractValue,
      delimiter,
      valueField,
      isUrl,
      env
    } = this.props;

    const files = isUrl ? this.files : this.files.filter(
      file => file.state == 'uploaded' || file.state == 'init'
    );

    let newValue: any = files.length
      ? joinValues
        ? files[0].value
        : files[0]
      : '';

    if (multiple) {
      newValue = joinValues
        ? files.map(item => item.value).join(delimiter)
        : extractValue
          ? files.map(item => item.value)
          : files;
    } else {
      newValue = joinValues
        ? newValue.value || newValue
        : extractValue
          ? newValue[valueField || 'value']
          : newValue;
    }
    if(isUrl) {
      newValue = files.map(item => {
        if(typeof item === 'string') {
          return item
        }
        if((item.addr || item.url)?.includes('http')) {
          return item.addr || item.url
        }
        return (env.axiosInstance?.defaults?.baseURL || env?.ajaxApi || '') + (item.addr || item.url)
      }).join(delimiter)
    }

    onChange((this.emitValue = newValue || ''), undefined, changeImmediately);
    this.syncAutoFill();
  }

  syncAutoFill() {
    const { autoFill, multiple, onBulkChange, data } = this.props;
    if (!isEmpty(autoFill) && onBulkChange) {
      const files = this.state.files.filter(
        file => ~['uploaded', 'init', 'ready'].indexOf(file.state as string)
      );
      const toSync = dataMapping(
        autoFill,
        multiple
          ? {
            items: files
          }
          : files[0]
      );

      Object.keys(toSync).forEach(key => {
        if (isPlainObject(toSync[key]) && isPlainObject(data[key])) {
          toSync[key] = merge({}, data[key], toSync[key]);
        }
      });
      onBulkChange(toSync);
    }
  }

  handleSelect() {
    this.dropzone.current && this.dropzone.current.open();
  }

  handleRetry(index: number) {
    const files = this.files.concat();
    const file = files[index];

    if (file.state !== 'invalid' && file.state !== 'error') {
      return;
    }

    file.state = 'pending';
    file.progress = 0;

    this.setState(
      {
        files: files
      },
      this.startUpload
    );
  }

  handleDrop(files: Array<FileX>) {
    const { multiple, crop } = this.props;

    if (crop && !multiple) {
      const file: any = files[0] as FileValue;
      if (!file.preview || !file.url) {
        file.preview = window.URL.createObjectURL(file);
      }

      return this.setState({
        cropFile: file,
        cropFileName: file.name
      });
    }

    this.addFiles(files);
  }

  handlePaste(e: React.ClipboardEvent<any>) {
    const event = e.nativeEvent as any;
    const files: Array<FileX> = [];
    const items = event.clipboardData.items;
    const accept = this.props.accept || '*';

    [].slice.call(items).forEach((item: DataTransferItem) => {
      let blob: FileX;

      if (
        item.kind !== 'file' ||
        !(blob = item.getAsFile() as File) ||
        !accepts(blob, accept)
      ) {
        return;
      }

      blob.id = guid();
      files.push(blob);
    });

    this.handleDrop(files);
  }

  handleCrop() {
    const { cropFormat, cropQuality } = this.props;
    this.cropper.getCroppedCanvas().toBlob(
      (file: File) => {
        this.addFiles([file]);
        this.setState({
          cropFile: undefined,
          locked: false,
          lockedReason: ''
        });
      },
      cropFormat || 'image/png',
      cropQuality || 1
    );
  }

  cancelCrop() {
    this.setState(
      {
        cropFile: undefined,
        cropFileName: undefined,
        locked: false,
        lockedReason: ''
      },
      this.onChange
    );
  }

  rotatableCrop() {
    this.cropper.rotate(45);
  }

  addFiles(files: Array<FileX>) {
    if (!files.length) {
      return;
    }

    const { multiple, maxLength, maxSize, accept, translate: __ } = this.props;
    let currentFiles = this.files;

    if (!multiple && currentFiles.length) {
      currentFiles = [];
    }

    const allowed =
      (multiple
        ? maxLength
          ? maxLength
          : files.length + currentFiles.length
        : 1) - currentFiles.length;
    const inputFiles: Array<FileX> = [];

    [].slice.call(files, 0, allowed).forEach((file: FileX) => {
      if (maxSize && file.size > maxSize) {
        this.props.env.alert(
          __('File.maxSize', {
            filename: file.name,
            actualSize: ImageControl.formatFileSize(file.size),
            maxSize: ImageControl.formatFileSize(maxSize)
          })
        );
        return;
      }

      file.state = 'pending';
      file.id = guid();
      if (!file.preview || !file.url) {
        file.preview = URL.createObjectURL(file);
      }
      inputFiles.push(file);
    });

    if (!inputFiles.length) {
      return;
    }

    this.setState(
      {
        error: undefined,
        files: (this.files = currentFiles.concat(inputFiles)),
        locked: true
      },
      () => {
        const { autoUpload } = this.props;

        if (autoUpload) {
          this.startUpload();
        }
      }
    );
  }

  sendFile(
    file: FileX,
    cb: (error: null | string, file: FileX, obj?: FileValue) => void,
    onProgress: (progress: number) => void
  ) {
    const { limit, translate: __ } = this.props;

    if (!limit) {
      return this._upload(file, cb, onProgress);
    }

    const image = new Image();
    image.onload = () => {
      const width = image.width;
      const height = image.height;
      let error = '';

      if (
        (limit.width && limit.width != width) ||
        (limit.height && limit.height != height)
      ) {
        error = __('Image.sizeNotEqual', {
          info: ImageControl.sizeInfo(limit.width, limit.height, __)
        });
      } else if (
        (limit.maxWidth && limit.maxWidth < width) ||
        (limit.maxHeight && limit.maxHeight < height)
      ) {
        error = __('Image.limitMax', {
          info: ImageControl.sizeInfo(limit.maxWidth, limit.maxHeight, __)
        });
      } else if (
        (limit.minWidth && limit.minWidth > width) ||
        (limit.minHeight && limit.minHeight > height)
      ) {
        error = __('Image.limitMin', {
          info: ImageControl.sizeInfo(limit.minWidth, limit.minHeight, __)
        });
      } else if (
        limit.aspectRatio &&
        Math.abs(width / height - limit.aspectRatio) > 0.01
      ) {
        error = __(limit.aspectRatioLabel || 'Image.limitRatio', {
          ratio: (+limit.aspectRatio).toFixed(2)
        });
      }

      if (error) {
        file.state = 'invalid';
        cb(error, file);
      } else {
        this._upload(file, cb, onProgress);
      }
    };
    image.src = (file.preview || file.url) as string;
  }

  async getFileMd5(file: any) {
    return new Promise<string>(resolve => {
      let spark = new SparkMD5.ArrayBuffer();
      let totalFileReader = new FileReader();

      totalFileReader.readAsArrayBuffer(file);
      totalFileReader.onload = function (e: any) {
        // 对整个totalFile生成md5
        spark.append(e.target.result)
        resolve(spark.end()) // 计算整个文件的fileMd5
      }
    })
  }

  handleFile = (config: any) => {
    return new Promise(async (resolve: any, reject: any) => {
      let fileSize = config?.size;
      let file = config;

      let blobSlice = File.prototype.slice; // 切片使用

      let _this = this;

      let chunkSize = 1024 * 1024 * 1;   // 切片每次1M
      let chunks = Math.ceil(file.size / chunkSize); // 切片数
      let currentChunk = 0; // 当前上传的chunk索引

      let file_type = config?.type;
      let file_name = config?.name;

      let spark = new SparkMD5.ArrayBuffer(); // 对arrayBuffer数据进行md5加密，产生一个md5字符串
      let chunkFileReader = new FileReader(); // 用于计算出每个chunkMd5

      let params: any = { chunks: [], file: {} };  // 用于上传所有分片的md5信息

      let arrayBufferData: any = [];  // 用于存储每个chunk的arrayBuffer对象,用于分片上传使用

      params.file.fileName = file.name || this.state.cropFileName;
      params.file.fileSize = file.size;
      const md5 = await this.getFileMd5(file)
      params.file.fileMd5 = md5
      params.file.id = config.id

      chunkFileReader.onload = function (e: any) {
        // 对每一片分片进行md5加密
        spark.append(e.target.result)
        // 每一个分片需要包含的信息
        let obj = {
          chunk: currentChunk + 1,
          start: currentChunk * chunkSize, // 计算分片的起始位置
          end: ((currentChunk * chunkSize + chunkSize) >= file.size) ? file.size : currentChunk * chunkSize + chunkSize, // 计算分片的结束位置
          chunkMd5: spark.end(),
          chunks
        }
        // 每一次分片onload,currentChunk都需要增加，以便来计算分片的次数
        currentChunk++;
        params.chunks.push(obj)

        // 将每一块分片的arrayBuffer存储起来，用来partUpload
        let tmp = {
          chunk: obj.chunk,
          currentBuffer: e.target.result
        }
        arrayBufferData.push(tmp)

        if (currentChunk < chunks) {
          // 当前切片总数没有达到总数时
          loadNext()
        } else {
          //记录所有chunks的长度
          params.file.fileChunks = params.chunks.length
          // 表示预处理结束，将上传的参数，arrayBuffer的数据存储起来
          _this.setState({
            // preUploading: false,
            uploadParams: params,
            arrayBufferData,
            chunksSize: chunks,
            // preUploadPercent: 100,
            file_type: file_type,
            upload_name: params.file.fileName,
            fileSize: fileSize
          }, () => {
            resolve()
          })
        }
      }

      chunkFileReader.onerror = function () {
        console.warn('oops, something went wrong.');
      };

      function loadNext() {
        var start = currentChunk * chunkSize,
          end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
        chunkFileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
      }

      loadNext();
    })
  }

  handlePartUpload = (originalFile: FileX, cb: Function, onProgress: (progress: number) => void) => {
    const { upload_cmd, upload_name, chunksSize, file_type, uploadParams } = this.state;
    const { chunks: uploadList, file } = uploadParams
    const { fileMd5 } = file;
    const { url, receiver, env, name } = this.props;
    let count = 0
    // 遍历uploadList
    try {
      // @ts-ignore
      Promise.allSettled(uploadList.map((_uploadItem: any) => {
        return new Promise((resolve: any, reject: any) => {
          const { chunkMd5, chunk, start, end } = _uploadItem;
          let formData = new FormData(),
            //新建一个Blob对象，将对应分片的arrayBuffer加入Blob中
            blob = new Blob([this.state.arrayBufferData[chunk - 1].currentBuffer], { type: 'application/octet-stream' });
          // 上传的参数
          let params = `fileMd5=${fileMd5}&chunkMd5=${chunkMd5}&chunk=${chunk}&start=${start}&end=${end}&chunks=${this.state.arrayBufferData.length}`

          // 通过formData将上传参数传入
          formData.append('upload_cmd', upload_cmd);
          formData.append('upload_field', String(name));
          formData.append('file_name', upload_name);
          formData.append('file_md5', fileMd5);
          formData.append('chunk_index', chunk);
          formData.append('chunk_total', String(chunksSize));
          formData.append('chunk_size', String(end - start));
          formData.append('type', file_type);
          formData.append('chunk_file', blob);
          // 搞不懂为什么，这样在壳上可以解密
          // Shell.hasShell() && Shell.uploadFile({ path: url || receiver, url: url || receiver, formData: formData, }, () => { }, () => { })

          env.fetcher({
            url: url || receiver,
            data: formData,
            method: 'post'
          }).then((res: any) => {
            if (res.status === 0) {
              count++
              onProgress(count / chunksSize)
              resolve(res?.data)
            } else {
              reject({ _err: res?.msg, _data: formData })
            }
          }).catch(e => reject(e))
        })
      })).then((_res: any) => {

        let _chunk_success_count: number = 0;
        let _chunk_error_arr: any = [];

        _res.map((_upload_res: any) => {
          if (_upload_res?.status === 'fulfilled') {
            _chunk_success_count++
          } else {
            _chunk_error_arr.push(_upload_res.reason._data)
          }
        })

        if (_chunk_success_count === chunksSize) {
          let formData = new FormData();
          // 通过formData将上传参数传入，发起文件分片合并通知
          formData.append('upload_cmd', 'chunk_merge');
          formData.append('upload_field', String(name));
          formData.append('file_name', upload_name);
          formData.append('file_md5', fileMd5);
          // 搞不懂为什么，这样在壳上可以解密
          // Shell.hasShell() && Shell.uploadFile({ path: url || receiver, url: url || receiver, formData: formData, }, () => { }, () => { })

          env.fetcher({
            url: url || receiver,
            data: formData,
            method: 'post'
          }).then((_merge_res: any) => {
            if (_merge_res?.status === 0) {
              const obj: FileValue = {
                ..._merge_res?.data,
                state: 'uploaded'
              };
              obj.value = obj.value || obj.url;
              cb(null, originalFile, obj);
            } else {
              cb(_merge_res?.msg, originalFile)
            }
          }).catch((_file_merge_res: any) => {
            cb('上传失败', originalFile)
            return;
          })
        } else {
          _chunk_error_arr.map((_chunk_error_item: any) => {
            env.fetcher({
              url: url,
              data: _chunk_error_item,
              method: 'post'
            }).then((_error_res: any) => {
            })
          })
          throw new Error('chunk file upload error!')
        }
      }).catch((_err: any) => {
        cb(_err, originalFile)
        return;
      })
    } catch (error) {
      cb(error, originalFile)
      return;
    }
  }

  _upload(
    file: Blob,
    cb: (error: null | string, file: Blob, obj?: FileValue) => void,
    onProgress: (progress: number) => void
  ) {
    const { url, receiver, env, name, translate: __ } = this.props;

    const formData = new FormData();
    this.handleFile(file).then(() => {
      const { uploadParams } = this.state;
      formData.append('upload_cmd', 'check_exist');
      formData.append('upload_field', String(name));
      formData.append('file_name', uploadParams?.file.fileName);
      formData.append('file_md5', uploadParams?.file.fileMd5);
      // 搞不懂为什么，这样在壳上可以解密
      // Shell.hasShell() && Shell.uploadFile({ path: url || receiver, url: url || receiver, formData: formData, }, () => { }, () => { })

      env.fetcher({
        url: url || receiver,
        data: formData,
        method: 'post',
        dataType: 'form-data'
      }).then((res: any) => {
        if (res?.status === 0 && res?.data) {
          const { exist } = res?.data;
          switch (exist) {
            case true:
              onProgress(1)
              const obj: FileValue = {
                ...res.data,
                state: 'uploaded'
              };
              obj.value = obj.value || obj.url;
              cb(null, file, obj);
              break;
            case false:
              this.setState({
                upload_cmd: 'chunk_upload'
              }, () => {
                // 上传分片文件
                this.handlePartUpload(file, cb, onProgress)
              });
              break;
            default:
              break;
          }
        } else {
          throw new Error(res.msg || __('File.errorRetry'));
        }
      }).catch(error => cb(error.message || __('File.errorRetry'), file))
    })
  }

  async _send(
    file: Blob,
    receiver: string,
    params: object,
    onProgress: (progress: number) => void
  ): Promise<Payload> {
    const fd = new FormData();
    const data = this.props.data;
    const api = buildApi(this.props.url || receiver, createObject(data, params), {
      method: 'post'
    });
    const fileField = this.props.fileField || 'file';

    const idx = api.url.indexOf('?');

    if (~idx && params) {
      params = {
        ...qsparse(api.url.substring(idx + 1)),
        ...params
      };
      api.url = api.url.substring(0, idx) + '?' + qsstringify(params);
    } else if (params) {
      api.url += '?' + qsstringify(params);
    }

    if (api.data) {
      qsstringify(api.data)
        .split('&')
        .filter(item => item !== '')
        .forEach(item => {
          let parts = item.split('=');
          fd.append(parts[0], decodeURIComponent(parts[1]));
        });
    }

    // Note: File类型字段放在后面，可以支持第三方云存储鉴权
    fd.append(fileField, file, (file as File).name || this.state.cropFileName);

    const env = this.props.env;

    if (!env || !env.fetcher) {
      throw new Error('fetcher is required');
    }

    try {
      return await env.fetcher(api, fd, {
        method: 'post',
        cancelExecutor: (cancelExecutor: () => void) => {
          // 记录取消器，取消的时候要调用
          this.fileUploadCancelExecutors.push({
            file: file,
            executor: cancelExecutor
          });
        },
        onUploadProgress: (event: { loaded: number; total: number }) =>
          onProgress(event.loaded / event.total)
      });
    } finally {
      this.removeFileCanelExecutor(file);
    }
  }

  removeFileCanelExecutor(file: any, execute = false) {
    this.fileUploadCancelExecutors = this.fileUploadCancelExecutors.filter(
      item => {
        if (execute && item.file === file) {
          item.executor();
        }

        return item.file !== file;
      }
    );
  }

  handleClick() {
    (this.refs.dropzone as any).open();
  }

  handleImageLoaded(index: number, e: React.UIEvent<any>) {
    const imgDom = e.currentTarget;
    const img = new Image();
    img.onload = () => {
      delete (img as any).onload;
      const files = this.files.concat();
      const file = files[index];

      if (!file) {
        return;
      }

      file.info = {
        ...file.info,
        width: img.width,
        height: img.height
      };

      files.splice(index, 1, file);
      const needUploading = !!(
        this.current || find(files, file => file.state === 'pending')
      );

      this.unmounted ||
        this.setState(
          {
            files: (this.files = files)
          },
          !needUploading ? this.onChange : undefined
        );
    };
    img.src = imgDom.src;
  }

  handleFrameImageLoaded(e: React.UIEvent<any>) {
    const imgDom = e.currentTarget;
    const img = new Image();
    const { clientHeight } = this.frameImageRef.current;

    const _this = this;
    img.onload = function () {
      const ratio = (this as any).width / (this as any).height;
      const finalWidth = (ratio * (clientHeight - 2)).toFixed(2);
      _this.setState({
        frameImageWidth: +finalWidth
      });
    };
    img.src = imgDom.src;
  }

  validate(): any {
    const __ = this.props.translate;

    if (this.state.locked && this.state.lockedReason) {
      return this.state.lockedReason;
    } else if (this.state.cropFile) {
      return new Promise(resolve => {
        this.resolve = resolve;
        this.handleCrop();
      });
    } else if (
      this.state.uploading ||
      this.files.some(item => item.state === 'pending')
    ) {
      return new Promise(resolve => {
        this.resolve = resolve;
        this.startUpload();
      });
    } else if (this.files.some(item => item.state === 'error')) {
      return __('File.errorRetry');
    }
  }

  addContent = () => (
    <div className='add-content-container'>
      <div className='add-content-body'>
        <div className='add-content-body-show'>将图片拖拽或复制至此即可添加</div>
        <div className='add-content-divider'/>
        <div className='add-content-body-input'>
          <div className='add-content-url'><Input value={this.state.pictureUrl} onChange={(e) => {
            this.setState({pictureUrl: e.target.value})
          }} placeholder='输入图片地址' /></div>
          <div className='add-content-commit'><Button onClick={async () => {
            const newFiles: any[] = this.state.files.concat();
            newFiles.push(this.state.pictureUrl);
            this.setState({files: newFiles}, () => {
              this.files = newFiles;
              this.onChange()
              this.setState({pictureUrl: ''})
            });
          }} disabled={!this.state.pictureUrl}>确认</Button></div>
          <div className='add-content-add'><Button onClick={this.handleSelect}>添加本地图片</Button></div>
        </div>
      </div>
    </div>
  )

  render() {
    const {
      className,
      classnames: cx,
      placeholder,
      disabled,
      multiple,
      accept,
      maxLength,
      autoUpload,
      hideUploadButton,
      thumbMode,
      thumbRatio,
      reCropable,
      frameImage,
      fixedSize,
      fixedSizeClassName,
      translate: __,
      isUrl
    } = this.props;
    const { files, error, crop, uploading, cropFile, frameImageWidth } =
      this.state;
    let frameImageStyle: any = {};
    if (fixedSizeClassName && frameImageWidth && fixedSize) {
      frameImageStyle.width = frameImageWidth;
    }
    const filterFrameImage = filter(frameImage, this.props.data, '| raw');

    const hasPending = files.some(file => file.state == 'pending');
    return (
      <div className={cx(`ImageControl`, className)}>
        {cropFile ? (
          <div className={cx('ImageControl-cropperWrapper')}>
            <Suspense fallback={<div>...</div>}>
              <Cropper
                {...crop}
                onInitialized={instance => {
                  this.cropper = instance;
                }}
                src={cropFile.preview}
              />
            </Suspense>
            <div className={cx('ImageControl-croperToolbar')}>
              {crop.rotatable && (
                <a
                  className={cx('ImageControl-cropRotatable')}
                  onClick={this.rotatableCrop}
                  data-tooltip={__('rotate')}
                  data-position="left"
                >
                  <Icon icon="retry" className="icon" />
                </a>
              )}
              <a
                className={cx('ImageControl-cropCancel')}
                onClick={this.cancelCrop}
                data-tooltip={__('cancel')}
                data-position="left"
              >
                <Icon icon="close" className="icon" />
              </a>
              <a
                className={cx('ImageControl-cropConfirm')}
                onClick={this.handleCrop}
                data-tooltip={__('confirm')}
                data-position="left"
              >
                <Icon icon="check" className="icon" />
              </a>
            </div>
          </div>
        ) : (
          <DropZone
            key="drop-zone"
            ref={this.dropzone}
            onDrop={this.handleDrop}
            onDropRejected={this.handleDropRejected}
            accept={accept}
            multiple={multiple}
            disabled={disabled}
          >
            {({
              getRootProps,
              getInputProps,
              isDragActive,
              isDragAccept,
              isDragReject,
              isFocused
            }) => {
              const addCon = <label
              className={cx(
                'ImageControl-addBtn',
                {
                  'is-disabled': disabled
                },
                fixedSize ? 'ImageControl-fixed-size' : '',
                fixedSize ? fixedSizeClassName : ''
              )}
              style={frameImageStyle}
              onClick={isUrl ? null : this.handleSelect}
              data-tooltip={__(placeholder)}
              data-position="right"
              ref={this.frameImageRef}
            >
              {filterFrameImage ? (
                <ImageComponent
                  key="upload-default-image"
                  src={filterFrameImage}
                  className={cx(
                    fixedSize ? 'Image-thumb--fixed-size' : ''
                  )}
                  onLoad={this.handleFrameImageLoaded.bind(this)}
                  thumbMode={thumbMode}
                  thumbRatio={thumbRatio}
                />
              ) : (
                <>
                  <Icon icon="plus" className="icon" />
                  <span>{__('File.upload')}</span>
                </>
              )}

              {isFocused ? (
                <span className={cx('ImageControl-pasteTip')}>
                  {__('Image.pasteTip')}
                </span>
              ) : null}
            </label>
            return (<div
                {...getRootProps({
                  onClick: preventEvent,
                  onPaste: this.handlePaste,
                  className: cx('ImageControl-dropzone', {
                    'is-disabled': disabled,
                    'is-empty': !files.length,
                    'is-active': isDragActive,
                    'input-img-mobile': isMobile()
                  })
                })}
              >
                <input {...getInputProps()} />

                {isDragActive || isDragAccept || isDragReject ? (
                  <div
                    className={cx('ImageControl-acceptTip', {
                      'is-accept': isDragAccept,
                      'is-reject': isDragReject
                    })}
                  >
                    {__('Image.dragDrop')}
                  </div>
                ) : (
                  <>
                    {files && files.length
                      ? files.map((file, key) => (
                        <div
                          key={file.id || key}
                          className={cx(
                            'ImageControl-item',
                            {
                              'is-uploaded': file.state !== 'uploading',
                              'is-invalid':
                                file.state === 'error' ||
                                file.state === 'invalid'
                            },
                            fixedSize ? 'ImageControl-fixed-size' : '',
                            fixedSize ? fixedSizeClassName : ''
                          )}
                          style={frameImageStyle}
                        >
                          {
                            typeof file === 'string' ? <div className='url-image-container' style={{padding: '0.25rem'}}>
                              <div className='url-img-mask' />
                              <a
                                data-tooltip={__('Image.zoomIn')}
                                data-position="bottom"
                                className='url-img-detail'
                                target="_blank"
                                rel="noopener"
                                href={file}
                                onClick={this.previewImage.bind(this,file,key)}
                              >
                                <Icon icon="view" className="icon" />
                              </a>
                              <a
                                data-tooltip={__('Select.clear')}
                                data-position="bottom"
                                className='url-img-del'
                                onClick={this.removeFile.bind(
                                  this,
                                  file,
                                  key
                                )}
                              >
                                  <Icon
                                    icon="remove"
                                    className="icon"
                                  />
                                </a>
                              <img style={{height: '100%', width: '100%', objectFit: 'contain'}} src={file} />
                            </div> :
                            file.state === 'invalid' ||
                            file.state === 'error' ? (
                            <>
                              <a
                                className={cx('ImageControl-itemClear')}
                                data-tooltip={__('Select.clear')}
                                data-position="bottom"
                                onClick={this.removeFile.bind(
                                  this,
                                  file,
                                  key
                                )}
                              >
                                <Icon icon="close" className="icon" />
                              </a>

                              <a
                                className={cx(
                                  'ImageControl-retryBtn',
                                  {
                                    'is-disabled': disabled
                                  },
                                  fixedSize ? 'ImageControl-fixed-size' : '',
                                  fixedSize ? fixedSizeClassName : ''
                                )}
                                onClick={this.handleRetry.bind(this, key)}
                              >
                                <Icon icon="retry" className="icon" />
                                <p className="ImageControl-itemInfoError">
                                  {__('File.repick')}
                                </p>
                              </a>
                            </>
                          ) : file.state === 'uploading' ? (
                            <>
                              <a
                                onClick={this.removeFile.bind(
                                  this,
                                  file,
                                  key
                                )}
                                key="clear"
                                className={cx('ImageControl-itemClear')}
                                data-tooltip={__('Select.clear')}
                              >
                                <Icon icon="close" className="icon" />
                              </a>
                              <div
                                key="info"
                                className={cx(
                                  'ImageControl-itemInfo',
                                  fixedSize ? 'ImageControl-fixed-size' : '',
                                  fixedSize ? fixedSizeClassName : ''
                                )}
                              >
                                <p>{__('File.uploading')}</p>
                                <div className={cx('ImageControl-progress')}>
                                  <span
                                    style={{
                                      width: `${Math.round(
                                        file.progress * 100
                                      )}%`
                                    }}
                                    className={cx(
                                      'ImageControl-progressValue'
                                    )}
                                  />
                                </div>
                                <p>{__('File.uploading')}</p>
                              </div>
                            </>
                          ) : (
                            <>
                              <ImageComponent
                                key="image"
                                className={cx(
                                  'ImageControl-image',
                                  fixedSize ? 'Image-thumb--fixed-size' : ''
                                )}
                                onLoad={this.handleImageLoaded.bind(
                                  this,
                                  key
                                )}
                                src={file.preview || file.url}
                                alt={file.name}
                                thumbMode={thumbMode}
                                thumbRatio={thumbRatio}
                                overlays={
                                  <>
                                    {file.info ? (
                                      [
                                        <div key="info">
                                          {file.info.width} x{' '}
                                          {file.info.height}
                                        </div>,
                                        file.info.len ? (
                                          <div key="size">
                                            {ImageControl.formatFileSize(
                                              file.info.len
                                            )}
                                          </div>
                                        ) : null
                                      ]
                                    ) : (
                                      <div>...</div>
                                    )}

                                    <a
                                      data-tooltip={__('Image.zoomIn')}
                                      data-position="bottom"
                                      target="_blank"
                                      rel="noopener"
                                      href={file.url || file.preview}
                                      onClick={this.previewImage.bind(
                                        this,
                                        file,
                                        key
                                      )}
                                    >
                                      <Icon icon="view" className="icon" />
                                    </a>

                                    {!!crop &&
                                      reCropable !== false &&
                                      !disabled ? (
                                      <a
                                        data-tooltip={__('Image.crop')}
                                        data-position="bottom"
                                        onClick={this.editImage.bind(
                                          this,
                                          key
                                        )}
                                      >
                                        <Icon
                                          icon="pencil"
                                          className="icon"
                                        />
                                      </a>
                                    ) : null}
                                    {!disabled ? (
                                      <a
                                        data-tooltip={__('Select.clear')}
                                        data-position="bottom"
                                        onClick={this.removeFile.bind(
                                          this,
                                          file,
                                          key
                                        )}
                                      >
                                        <Icon
                                          icon="remove"
                                          className="icon"
                                        />
                                      </a>
                                    ) : null}
                                    <a
                                      data-tooltip={
                                        file.name ||
                                        getNameFromUrl(file.value || file.url)
                                      }
                                      data-position="bottom"
                                      target="_blank"
                                    >
                                      <Icon icon="info" className="icon" />
                                    </a>
                                  </>
                                }
                              />
                            </>
                          )
                          }
                        </div>
                      ))
                      : null}

                    {(multiple && (!maxLength || files.length < maxLength)) ||
                      (!multiple && !files.length) ? (
                        isUrl ? <Popover placement='right' content={this.addContent()} trigger="click" 
                          getPopupContainer={() => document.getElementById('amis-modal-container')! || document.body}>
                          {addCon}
                        </Popover> : addCon
                    ) : null}

                    {!autoUpload && !hideUploadButton && files.length ? (
                      <Button
                        level="default"
                        className={cx('ImageControl-uploadBtn')}
                        disabled={!hasPending}
                        onClick={this.toggleUpload}
                      >
                        {__(uploading ? 'File.pause' : 'File.start')}
                      </Button>
                    ) : null}

                    {error ? (
                      <div className={cx('ImageControl-errorMsg')}>{error}</div>
                    ) : null}
                  </>
                )}
              </div>
            )}}
          </DropZone>
        )}
      </div>
    );
  }
}

@FormItem({
  type: 'input-image',
  sizeMutable: false
})
export class ImageControlRenderer extends ImageControl { }
