import * as React from "react";
import { useDropzone } from "react-dropzone";
import { toast } from "sonner";
import { useGetSet } from "../../hooks";
import { useGetKey } from "../../hooks/useGetKey";
import { mergeDefaults } from "../../utils/mergeDefaults";
import { setRef } from "../../utils/mergeRefs";

enum AcceptType {
  Image = "image/*",
  Video = "video/*",
  File = "application/*",
}

const defaultExtensions: Record<AcceptType, string> = {
  [AcceptType.Image]: [
    ".jpg",
    ".jpeg",
    ".png",
    ".svg",
    ".gif",
    ".webp",
    ".avif",
  ].join(","),
  [AcceptType.Video]: [".mp4", ".webm", ".mov", ".avi", ".mkv"].join(","),
  [AcceptType.File]: "",
};

type Dimensions = {
  width: number;
  height: number;
};
function getImageSize(file: File): Promise<Dimensions> {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = function onload() {
      const { width, height } = img;
      resolve({ width, height });
    };
    img.onerror = reject;
    img.src = URL.createObjectURL(file);
  });
}

export interface FileProps extends React.HTMLAttributes<HTMLDivElement> {
  children?: React.ReactNode;
  "data-id": string;
  allowMultiple?: boolean;
  minSize?: number;
  maxSize?: number;
  acceptType?: AcceptType;
  handleDrop?: (files: File[] | File) => Promise<void>;
  onSuccess?: (e: Event) => void;
  extensions?: string;
  required?: boolean;
  capture?: "user" | "environment" | "implementation";
}

function rafDebounce(fn) {
  let raf;

  return (...args) => {
    if (raf) {
      return;
    }

    raf = window.requestAnimationFrame(() => {
      fn(...args); // run useful code
      raf = undefined;
    });
  };
}

const BREVITY_UPLOAD_URL = "/api/upload/public";
const noop = () => {};
const MAX_FILE_SIZE = 1024 * 1024 * 1024 * 4; // 4 GB in bytes
const DEFAULT_VALUE = {
  file: null,
  error: false,
  progress: 0,
};
export const FileUpload = React.forwardRef(function FileUpload(
  args: FileProps,
  ref,
) {
  const {
    children,
    allowMultiple,
    minSize,
    maxSize,
    acceptType,
    handleDrop,
    onSuccess,
    extensions,
    required,
    capture,
    ...props
  } = mergeDefaults(
    args,
    {
      allowMultiple: false,
      minSize: 0,
      maxSize: MAX_FILE_SIZE,
      acceptType: AcceptType.Image,
      handleDrop: noop as any,
      onSuccess: noop,
      required: false,
      extensions: defaultExtensions[args.acceptType ?? AcceptType.File],
    },
    true,
  );
  const key = useGetKey(props);
  const [_, setState] = useGetSet(key, DEFAULT_VALUE);
  const onDrop = React.useCallback(
    async (acceptedFiles: File[]) => {
      if (acceptedFiles.length === 0) {
        inputRef.current?.setCustomValidity(
          allowMultiple
            ? "Please upload one or more files"
            : "Please upload a file",
        );
        return;
      }
      inputRef.current?.setCustomValidity("");
      const maybeFiles = allowMultiple ? acceptedFiles : acceptedFiles[0];
      await handleDrop?.(maybeFiles);
      let payload: File | FormData;
      const request = new XMLHttpRequest();
      const headers: Array<[string, string]> = [];
      if (allowMultiple) {
        payload = new FormData();
        acceptedFiles.forEach((file) => {
          (payload as FormData).append("file", file);
        });
      } else {
        payload = acceptedFiles[0];
        if (payload.type.startsWith("image")) {
          const { width, height } = await getImageSize(payload);
          headers.push(["X-Image-Width", width.toString()]);
          headers.push(["X-Image-Height", height.toString()]);
        }
      }
      const method = allowMultiple ? "POST" : "PUT";
      const url = allowMultiple
        ? BREVITY_UPLOAD_URL
        : `${BREVITY_UPLOAD_URL}/${(payload as File).name}`;
      request.open(method, url);
      headers.forEach(([key, value]) => {
        request.setRequestHeader(key, value);
      });
      request.upload.addEventListener(
        "progress",
        rafDebounce((e) => {
          const percent_completed = (e.loaded / e.total) * 100;
          setState({ progress: percent_completed });
        }),
      );
      request.addEventListener("load", (e) => {
        if (request.readyState === request.DONE && request.status < 300) {
          const response = JSON.parse(request.responseText);
          setState({ progress: 100, file: response, error: false });
          onSuccess?.(e);
        } else {
          setState({ progress: 0, file: null, error: true });
        }
      });
      // send POST request to server
      request.send(payload);
    },
    [allowMultiple, handleDrop, onSuccess],
  );
  const accept = React.useMemo(() => {
    if (acceptType && extensions) {
      return {
        accept: {
          [acceptType]: extensions.split(",").map((ext: string) => ext.trim()),
        },
      };
    }
    return {};
  }, [acceptType, extensions]);
  const { getRootProps, getInputProps, isDragActive, inputRef, rootRef } =
    useDropzone({
      ...accept,
      minSize,
      maxSize,
      multiple: allowMultiple,
      onDrop,
      onError(err) {
        toast.error(`Error uploading file`, {
          description: err.message,
        });
      },
    });
  setRef(ref, rootRef.current);
  React.useEffect(() => {
    const parentForm = inputRef.current?.form;
    const handler = () => {
      setState(DEFAULT_VALUE);
    };
    parentForm?.addEventListener("reset", handler);
    return () => {
      parentForm?.removeEventListener("reset", handler);
    };
  }, []);
  React.useEffect(() => {
    inputRef.current?.setCustomValidity(
      required
        ? allowMultiple
          ? "Please upload one or more files"
          : "Please upload a file"
        : "",
    );
  }, [required]);

  return (
    <div
      {...getRootProps({ ...props, ["data-id"]: key })}
      data-drag-active={isDragActive}
    >
      <input
        {...getInputProps({
          capture:
            capture === "implementation" ? undefined : (capture ?? undefined),
        })}
        style={{
          display: "initial",
          opacity: "0",
          width: "1px",
          height: "1px",
          margin: "-1px",
          clip: "rect(0, 0, 0, 0)",
          position: "absolute",
          zIndex: -1,
        }}
      />
      {children}
    </div>
  );
});
