import * as React from 'react';
import { useClientSide, useFallbackId, useProxyInput } from '@modal-sh/react-utils';
import clsx from 'clsx';

const DEFAULT_ENHANCED_HEIGHT_PX = 64 as const;

const DEFAULT_NON_ENHANCED_SIDE_HEIGHT_PX = 256 as const;

/**
 * Common props for the {@link FileSelectBoxProps.previewComponent|previewComponent prop} of the
 * {@link FileSelectBox} component.
 */
export interface CommonPreviewComponentProps<F extends Partial<File> = Partial<File>> {
  /**
   * The file to preview.
   */
  file?: F;
  /**
   * Is the component disabled?
   */
  disabled?: boolean;
  /**
   * Should the component be enhanced?
   */
  enhanced?: boolean;
  /**
   * Should the component use minimal space?
   */
  mini?: boolean;
}

/**
 * Derived HTML element of the {@link FileSelectBox} component.
 */
export type FileSelectBoxDerivedElement = HTMLInputElement;

/**
 * Props of the {@link FileSelectBox} component.
 */
export interface FileSelectBoxProps<
  F extends Partial<File> = Partial<File>,
  P extends CommonPreviewComponentProps<F> = CommonPreviewComponentProps<F>
> extends Omit<React.HTMLProps<FileSelectBoxDerivedElement>, 'size' | 'type' | 'label' | 'list'> {
  /**
   * Should the component display a border?
   */
  border?: boolean,
  /**
   * Should the component occupy the whole width of its parent?
   */
  block?: boolean,
  /**
   * Short textual description indicating the nature of the component's value.
   */
  label?: React.ReactNode,
  /**
   * Short textual description as guidelines for valid input values.
   */
  hint?: React.ReactNode,
  /**
   * Should the component be enhanced?
   */
  enhanced?: boolean,
  /**
   * Is the label hidden?
   */
  hiddenLabel?: boolean,
  /**
   * Preview component for the selected file(s).
   */
  previewComponent?: React.ElementType<P>,
  /**
   * Reselect label.
   */
  reselectLabel?: string,
  /**
   * Clear label.
   */
  clearLabel?: string,
}

export type FileSelectBoxDefaultPreviewComponentDerivedElement = HTMLDivElement;

/**
 * Default component for the {@link FileSelectBoxProps.previewComponent|previewComponent prop}
 * of the {@link FileSelectBox} component.
 */
export const FileSelectBoxDefaultPreviewComponent = React.forwardRef<
  FileSelectBoxDefaultPreviewComponentDerivedElement,
  CommonPreviewComponentProps
>(({
  file,
  mini,
  enhanced,
  disabled,
}, forwardedRef) => (
  <div
    data-enhanced={enhanced}
    className={clsx({
      'opacity-50': disabled,
    })}
    ref={forwardedRef}
  >
    <div className="w-full whitespace-nowrap overflow-hidden text-ellipsis">
      {file?.name ?? (
        <span className="opacity-50">
          File
        </span>
      )}
    </div>
    {!mini && (
      <>
        {typeof file?.type === 'string' && (
          <div className="w-full whitespace-nowrap overflow-hidden text-ellipsis">{file?.type}</div>
        )}
        {typeof file?.size === 'number' && (
          <div
            title={new Intl.NumberFormat(undefined, {
              style: 'unit',
              unit: 'byte',
              unitDisplay: 'long',
            }).format(file.size)}
            className="w-full whitespace-nowrap overflow-hidden text-ellipsis tabular-nums"
          >
            {new Intl.NumberFormat(undefined, {
              style: 'unit',
              unit: 'kilobyte',
              unitDisplay: 'long',
            }).format(file.size)}
          </div>
        )}
        {typeof file?.lastModified === 'number' && (
          <div className="w-full whitespace-nowrap overflow-hidden text-ellipsis">
            <time dateTime={new Date(file.lastModified).toISOString()}>
              {new Date(file.lastModified).toDateString()}
            </time>
          </div>
        )}
      </>
    )}
  </div>
));

const isButtonElement = (el: HTMLElement): el is HTMLButtonElement => el.tagName === 'BUTTON';

const isInputElement = (el: HTMLElement): el is HTMLInputElement => el.tagName === 'INPUT';

const isKeyUpEvent = (e: React.SyntheticEvent): e is React.KeyboardEvent => e.type === 'keyup';

const isMouseUpEvent = (e: React.SyntheticEvent): e is React.MouseEvent => e.type === 'mouseup';

const isDropEvent = (e: React.SyntheticEvent): e is React.DragEvent => e.type === 'drop';

const isPasteEvent = (e: React.SyntheticEvent): e is React.ClipboardEvent => e.type === 'paste';

export const DELETE_KEYS = ['Backspace', 'Delete'] as const;

type DeleteKey = typeof DELETE_KEYS[number];

export const SELECT_KEYS = ['Enter', 'Space', 'Return'] as const;

type SelectKey = typeof SELECT_KEYS[number];

/**
 * Component for selecting files.
 */
export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileSelectBoxProps>((
  {
    label = '',
    hint = '',
    border = false,
    block = false,
    enhanced: enhancedProp = false,
    hiddenLabel = false,
    multiple = false,
    disabled = false,
    className,
    onChange,
    id: idProp,
    placeholder,
    previewComponent: FilePreviewComponent = FileSelectBoxDefaultPreviewComponent,
    style,
    onBlur,
    reselectLabel = 'Reselect',
    clearLabel = 'Clear',
    onPaste,
    ...etcProps
  },
  forwardedRef,
) => {
  const { clientSide } = useClientSide({ clientSide: enhancedProp });
  const [fileList, setFileList] = React.useState<FileList>();
  const [lastUpdated, setLastUpdated] = React.useState<number>();
  const clearFileListRef = React.useRef(false);
  const [aboutToClear, setAboutToClear] = React.useState(false);
  const [aboutToSelect, setAboutToSelect] = React.useState(false);

  const clearFiles = (
    fileInput: FileSelectBoxDerivedElement,
    clearRef: React.MutableRefObject<boolean>,
  ) => {
    const clearRefMut = clearRef as unknown as Record<string, boolean>;
    clearRefMut.current = true;

    const fileInputMut = fileInput as unknown as Record<string, string>;
    fileInputMut.value = '';

    setFileList(undefined);
    setLastUpdated(Date.now());
  };

  const { defaultRef, handleChange: doSetFileList } = useProxyInput<
    React.ChangeEvent<FileSelectBoxDerivedElement>
    | React.MouseEvent<HTMLButtonElement>
    | React.DragEvent<HTMLDivElement>
    | React.KeyboardEvent<FileSelectBoxDerivedElement>
    | React.ClipboardEvent<FileSelectBoxDerivedElement>,
    FileSelectBoxDerivedElement
  >({
    forwardedRef,
    valueSetterFn: (e) => {
      const fileInput = defaultRef.current as FileSelectBoxDerivedElement;
      if (isMouseUpEvent(e) && isButtonElement(e.currentTarget)) {
        clearFiles(fileInput, clearFileListRef);
        return;
      }
      if (isKeyUpEvent(e) && isInputElement(e.currentTarget) && e.currentTarget === fileInput) {
        // delete via keyboard
        const { code } = e;

        if (DELETE_KEYS.includes(code as DeleteKey)) {
          clearFiles(fileInput, clearFileListRef);
          setAboutToClear(false);
        }

        return;
      }
      if (isDropEvent(e)) {
        // drop file
        const { dataTransfer } = e;
        const { files } = dataTransfer;
        if (files && files.length > 0) {
          try {
            fileInput.files = files;
          } catch {
            // noop, the assignment throws for test environments
          }
          setFileList(files);
          setLastUpdated(Date.now());
        }
        return;
      }
      if (isPasteEvent(e)) {
        const { clipboardData } = e;
        const { files } = clipboardData;
        if (files && files.length > 0) {
          try {
            fileInput.files = files;
          } catch {
            // noop, the assignment throws for test environments
          }
          setFileList(files);
          setLastUpdated(Date.now());
        }
        return;
      }
      // Unhandled event
    },
  });
  const labelId = React.useId();
  const id = useFallbackId(idProp);

  const cancelEvent: React.DragEventHandler<HTMLDivElement> = React.useCallback((e) => {
    e.stopPropagation();
    e.preventDefault();
  }, []);

  const handleDrop: React.DragEventHandler<HTMLDivElement> = React.useCallback((e) => {
    cancelEvent(e);
    doSetFileList(e);
  }, [cancelEvent, doSetFileList]);

  const handleFileChange: React.ChangeEventHandler<
    FileSelectBoxDerivedElement
  > = React.useCallback((e) => {
    const { currentTarget } = e;
    if (currentTarget.files && currentTarget.files.length > 0) {
      setFileList(currentTarget.files);
      setLastUpdated(Date.now());
      setAboutToSelect(false);
    } else if (clearFileListRef.current) {
      clearFileListRef.current = false;
      setFileList(undefined);
      setLastUpdated(Date.now());
    }
    onChange?.(e);
  }, [clearFileListRef, onChange]);

  const handleKeyDown: React.KeyboardEventHandler<
    FileSelectBoxDerivedElement
  > = React.useCallback((e) => {
    const { code } = e;
    if (DELETE_KEYS.includes(code as DeleteKey)) {
      setAboutToClear(true);
      return;
    }

    if (SELECT_KEYS.includes(code as SelectKey)) {
      setAboutToSelect(true);
    }
    // ignore other keys
  }, []);

  const handleReselectMouseDown: React.MouseEventHandler<
    HTMLButtonElement
  > = React.useCallback(() => {
    setAboutToSelect(true);
    setTimeout(() => {
      const fileInput = defaultRef.current as FileSelectBoxDerivedElement;
      fileInput.focus();
    });
  }, [defaultRef]);

  const handleReselectMouseUp: React.MouseEventHandler<
    HTMLButtonElement
  > = React.useCallback(() => {
    const fileInput = defaultRef.current as FileSelectBoxDerivedElement;
    if (typeof fileInput.showPicker !== 'function') {
      fileInput.click();
      return;
    }
    fileInput.showPicker();
  }, [defaultRef]);

  const handleDeleteMouseDown: React.MouseEventHandler<
    HTMLButtonElement
  > = React.useCallback(() => {
    setAboutToClear(true);
    setTimeout(() => {
      const fileInput = defaultRef.current as FileSelectBoxDerivedElement;
      fileInput.focus();
    });
  }, [defaultRef]);

  const handleBlur: React.FocusEventHandler<
    FileSelectBoxDerivedElement
  > = React.useCallback((e) => {
    setAboutToSelect(false);
    setAboutToClear(false);
    onBlur?.(e);
  }, [onBlur]);

  React.useEffect(() => {
    const handleLabelMouseUp = () => {
      setAboutToSelect(false);
      setAboutToClear(false);
    };

    window.addEventListener('mouseup', handleLabelMouseUp, { capture: true });
    return () => {
      window.removeEventListener('mouseup', handleLabelMouseUp, { capture: true });
    };
  }, [defaultRef, aboutToSelect]);

  const filesCount = fileList?.length ?? 0;
  const fileListArray = Array.from(fileList ?? []);

  return (
    <div
      className={clsx(
        'relative rounded ring-secondary/50 group file-select-box',
        'focus-within:ring-4',
        block && 'flex w-full',
        !block && 'inline-flex w-64 min-h-16 justify-center items-center',
        className,
      )}
      onDragEnter={clientSide ? cancelEvent : undefined}
      onDragOver={clientSide ? cancelEvent : undefined}
      onDrop={clientSide ? handleDrop : undefined}
      data-testid="root"
      style={{
        height: clientSide ? DEFAULT_ENHANCED_HEIGHT_PX : undefined,
        ...(style ?? {}),
      }}
    >
      {label && (
        <label
          data-testid="label"
          id={labelId}
          htmlFor={id}
          className={clsx(
            'absolute z-[1] w-full top-0.5 left-0 pointer-events-none select-none pl-1 text-xxs font-bold peer-disabled:opacity-50 group-focus-within:text-secondary text-primary leading-none bg-negative',
            {
              'sr-only': hiddenLabel,
            },
          )}
        >
          <div className="w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
            {label}
          </div>
        </label>
      )}
      {clientSide && (
        <button
          type="button"
          disabled={disabled}
          tabIndex={-1}
          className={clsx(
            'flex items-center focus:outline-0 justify-center absolute top-0 left-0 w-full h-full cursor-pointer select-none',
            (fileList?.length ?? 0) > 0 && 'opacity-0',
          )}
          data-testid="clickArea"
          onMouseDown={handleReselectMouseDown}
          onMouseUp={handleReselectMouseUp}
        >
          {placeholder}
        </button>
      )}
      <input
        {...etcProps}
        id={id}
        disabled={disabled}
        ref={defaultRef}
        type="file"
        onBlur={handleBlur}
        onKeyDown={clientSide ? handleKeyDown : undefined}
        onKeyUp={clientSide ? doSetFileList : undefined}
        className={clsx(
          'peer box-border focus:outline-0 px-4 pt-2 block resize-y min-h-16 cursor-pointer disabled:cursor-not-allowed file:bg-transparent file:text-primary file:block file:font-bold file:font-semi-expanded file:uppercase file:p-0 file:border-0 group-focus-within:file:text-secondary',
          {
            'sr-only': clientSide,
            'h-full w-full': !clientSide,
          },
        )}
        onChange={clientSide ? handleFileChange : onChange}
        onPaste={clientSide ? doSetFileList : onPaste}
        multiple={multiple}
        data-testid="input"
        aria-labelledby={label ? `${labelId}` : undefined}
        style={{
          height: clientSide ? undefined : DEFAULT_NON_ENHANCED_SIDE_HEIGHT_PX,
        }}
      />
      {filesCount < 1
      && clientSide
      && hint
      && (
        <div
          data-testid="hint"
          className="absolute top-0 left-0 w-full h-full pointer-events-none box-border overflow-hidden pt-4"
        >
          <div className="flex items-center justify-center w-full h-full">
            {hint}
          </div>
        </div>
      )}
      {filesCount > 0
      && clientSide
      && (
        <React.Fragment
          key={lastUpdated}
        >
          <div
            className="sm:absolute top-0 left-0 w-full h-full pointer-events-none pb-12 box-border overflow-hidden pt-8"
            data-testid="preview"
          >
            {multiple
            && (
              <div className="pointer-events-auto w-full h-full overflow-auto px-4 box-border">
                <div className="w-full grid gap-2 grid-cols-3">
                  {fileListArray.map((file, i) => (
                    <div
                      key={file.name ?? i}
                      data-testid="selectedFileItem"
                      className="w-full p-2 aspect-square rounded overflow-hidden relative before:absolute before:content-[''] before:bg-current before:top-0 before:left-0 before:w-full before:h-full before:opacity-10"
                    >
                      <FilePreviewComponent
                        file={file}
                        enhanced={clientSide}
                        disabled={disabled}
                        mini
                      />
                    </div>
                  ))}
                </div>
              </div>
            )}
            {!multiple
            && fileListArray.map((file, i) => (
              <div
                key={file.name ?? i}
                className="pointer-events-auto w-full h-full px-4 box-border"
              >
                <div
                  data-testid="selectedFileItem"
                  className="h-full w-full p-4 box-border rounded overflow-hidden relative before:absolute before:content-[''] before:bg-current before:top-0 before:left-0 before:w-full before:h-full before:opacity-10"
                >
                  <div
                    className="w-full h-full relative"
                  >
                    <FilePreviewComponent
                      file={file}
                      enhanced={clientSide}
                      disabled={disabled}
                    />
                  </div>
                </div>
              </div>
            ))}
          </div>
          <div
            data-testid="actions"
            className="absolute bottom-0 left-0 w-full text-center h-12 box-border flex"
          >
            <div className="w-0 flex-auto flex flex-col items-center justify-center h-full">
              <button
                type="button"
                data-testid="reselect"
                disabled={disabled}
                onMouseDown={handleReselectMouseDown}
                onMouseUp={handleReselectMouseUp}
                tabIndex={-1}
                className={clsx(
                  'flex w-full h-full focus:outline-0 bg-negative text-primary cursor-pointer items-center justify-center leading-none gap-4 select-none',
                  {
                    'group-focus-within:text-secondary group-focus-within:active:text-tertiary': !aboutToSelect,
                    'text-tertiary': aboutToSelect,
                  },
                )}
              >
                <span
                  className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded"
                >
                  {reselectLabel}
                </span>
              </button>
            </div>
            <div className="w-0 flex-auto flex flex-col items-center justify-center h-full">
              <button
                disabled={disabled}
                data-testid="clear"
                type="button"
                name="action"
                value="clear"
                onMouseDown={handleDeleteMouseDown}
                onMouseUp={doSetFileList}
                tabIndex={-1}
                className={clsx(
                  'flex w-full h-full bg-negative text-primary disabled:text-primary items-center justify-center leading-none gap-4 select-none focus:outline-0',
                  {
                    'group-focus-within:text-secondary group-focus-within:active:text-tertiary': !aboutToClear,
                    'text-tertiary': aboutToClear,
                  },
                )}
              >
                <span
                  className="block uppercase font-bold h-[1.1em] w-full whitespace-nowrap overflow-hidden text-ellipsis font-semi-expanded"
                >
                  {clearLabel}
                </span>
              </button>
            </div>
          </div>
        </React.Fragment>
      )}
      {border && (
        <span
          data-testid="border"
          className="absolute z-[1] peer-disabled:opacity-50 inset-0 rounded-inherit border-2 border-primary pointer-events-none group-focus-within:border-secondary"
        />
      )}
    </div>
  );
});

FileSelectBox.displayName = 'FileSelectBox';

FileSelectBox.defaultProps = {
  border: false as const,
  block: false as const,
  enhanced: false as const,
  hiddenLabel: false as const,
  label: undefined,
  hint: undefined,
  previewComponent: FileSelectBoxDefaultPreviewComponent,
  reselectLabel: 'Reselect' as const,
  clearLabel: 'Clear' as const,
};

FileSelectBoxDefaultPreviewComponent.defaultProps = {
  file: undefined,
  mini: false as const,
  disabled: FileSelectBox.defaultProps.disabled,
  enhanced: FileSelectBox.defaultProps.enhanced,
};
