import { isArray, isString, last } from 'lodash';
import React, { useRef, useState } from 'react';
import { useDropZone, useFormField } from '../../hooks';
import { useSilkeContext } from '../../silke-theme-provider';
import { BoxLabelProps, SilkeBox } from '../silke-box';
import { SilkeButton } from '../silke-button';
import { FormFieldProps, FormValidator } from '../silke-form';
import { SilkeSkeleton } from '../silke-skeleton';
import { SilkeText, SilkeTitle } from '../silke-text';
import { SilkeIcon } from '../silke-icon';

import styles from './silke-upload-field.scss';
import { SilkePopover } from '../silke-popover';
import { SilkeTooltip } from '../silke-tooltip';

export type FileModel = {
  size?: number;
  name?: string;
  url: string;
  type?: string;
};

type PendingFile = Omit<FileModel, 'url'> & {
  url?: string;
  error?: string;
  task: Promise<void>;
  stopped?: boolean;
};

type BaseProps = {
  accept?: string;
  help?: React.ReactNode;
  /** Max file size in KB */
  maxSize?: number;
  disabled?: boolean;
  required?: boolean;
  useFileModel?: boolean;
  multiple?: boolean;
  autoFocus?: boolean;
  onSelectFiles?: (value: any) => void;
} & Omit<BoxLabelProps, 'onChange'>;

type SingleField = { multiple?: false | undefined } & BaseProps &
  FormFieldProps<FileModel | string>;
type MultipleField = { multiple: true } & BaseProps & FormFieldProps<(FileModel | string)[]>;

type SilkeUploadFieldProps = (SingleField | MultipleField) & {
  dropzoneStyle?: React.CSSProperties;
  fileItemStyle?: React.CSSProperties;
};

const validate: FormValidator<SilkeUploadFieldProps> = (props, value) => {
  if (props.required && value === undefined) return 'Required';
};

export function SilkeUploadField(props: SilkeUploadFieldProps) {
  const { onFileUpload } = useSilkeContext();
  const [pending, setPending] = useState<PendingFile[]>([]);
  const size = props.size || 'base';

  const {
    useFileModel,
    accept,
    maxSize,
    disabled,
    autoFocus,
    name,
    help,
    multiple,
    readOnly,
    value,
    label,
    error,
    onFocus,
    onBlur,
    onChange,
    dropzoneStyle,
    fileItemStyle,
  } = useFormField(props, validate);
  const ref = useRef<HTMLLabelElement>(null);
  const pendingRef = useRef<PendingFile[]>([]);
  const valueRef = useRef<SilkeUploadFieldProps['value']>();
  const [dropState, setDropState] = useState<null | 'over' | 'invalid'>(null);

  pendingRef.current = pending;
  valueRef.current = value;
  const handleFileUploaded = (model: PendingFile, url: string) => {
    // Do not continue if upload stopped
    if (model.stopped) return;

    const file = useFileModel
      ? ({ name: model.name, size: model.size, type: model.type, url } as FileModel)
      : url;
    if (props.multiple) {
      const value = valueRef.current;
      onChange(isArray(value) ? [...value, file] : [file]);
    } else {
      onChange(file);
    }

    const pending = pendingRef.current || [];
    setPending(pending.filter((m) => m !== model));
  };

  const handleFiles = (files?: FileList | null) => {
    if (!files) return;
    const fileLen = multiple ? files.length : 1;
    const uploadFiles: PendingFile[] = [];
    // takes the acceptable file extensions (string), cleans them, and formats them into an array.
    const regParts = accept?.split(',').map((r) => r.trim().replace('/*', '/.*'));
    // regex explained:
    // ^: assert position at start of line.
    // .: matches any character.
    // *: matches previous token between zero and unlimited times.
    // (regParts.join('|')): matches any of the string values in regParts[]. '|' acts like an "or".
    // $: asserts position at the end of a line.
    const regToMatchAcceptableFileTypes = regParts && `^.*(${regParts.join('|')})$`;
    const acceptReg = regToMatchAcceptableFileTypes && new RegExp(regToMatchAcceptableFileTypes);

    for (let i = 0; i < fileLen; i++) {
      const file = files.item(i);
      if (file) {
        const tooLarge = maxSize && file.size / 1024 > maxSize;
        const invalidFormat = acceptReg && !acceptReg.test(file.type) && !acceptReg.test(file.name);
        const model: PendingFile = {
          name: file.name,
          size: file.size,
          type: file.type,
          task:
            tooLarge || invalidFormat
              ? Promise.resolve()
              : onFileUpload(file)
                  .then((url) => handleFileUploaded(model, url))
                  .catch((e) => {
                    console.error('Failed to upload file', file.name, e);
                    const pending = (pendingRef.current || []).slice();
                    const index = pending.indexOf(model);
                    if (index !== -1) {
                      pending[index] = { ...pending[index], error: 'Failed to upload' };
                      setPending(pending);
                    }
                  }),
        };

        if (invalidFormat) model.error = 'Not a valid format';
        else if (tooLarge) model.error = `The file is too large, max size is ${maxSize / 1024}MB`;
        uploadFiles.push(model);
      }
    }
    // if (!props.multiple) onChange(null);
    setPending([...pending, ...uploadFiles]);
  };

  const handleDragOver = () => {
    setDropState('over');
  };

  const handleDragLeave = () => {
    setDropState(null);
  };

  const handleDrop = (e: DragEvent) => {
    handleFiles(e.dataTransfer?.files);
    setDropState(null);
  };

  useDropZone(ref, handleDrop, handleDragOver, handleDragLeave);
  let cl = styles.root;
  if (dropState) cl += ' ' + styles[dropState];
  return (
    <SilkeBox column gap="s">
      <SilkeText weight="strong">{label}</SilkeText>
      <SilkeBox tag="label" column gap="s" className={cl} onFocus={onFocus} onBlur={onBlur}>
        <input
          type="file"
          disabled={disabled}
          accept={accept}
          autoFocus={autoFocus}
          maxLength={1024}
          name={name}
          multiple={multiple}
          onChange={(e) => {
            if (props.onSelectFiles) return props.onSelectFiles(e.currentTarget.files);
            handleFiles(e.currentTarget.files);
          }}
        />
        <SilkeBox
          pad="m"
          gap="m"
          className={styles.drop}
          ref={ref}
          column
          bg="neutral-10"
          style={dropzoneStyle}
          hAlign="center"
        >
          <SilkeIcon icon="file.arrow.up" className={styles.icon} />
          <SilkeBox column gap="s">
            <SilkeTitle kind={size === 'xs' ? 'xxs' : 'xs'}>
              Select a file <span style={{ color: 'white' }}>drag and drop here</span>
            </SilkeTitle>
            {help && !error && (
              <SilkeText size={size === 'xs' ? 'small' : 'base'} color="neutral-80">
                {help}
              </SilkeText>
            )}
            {error && (
              <SilkeText size={size === 'xs' ? 'small' : 'base'} color="error-30">
                {error}
              </SilkeText>
            )}
          </SilkeBox>
        </SilkeBox>
      </SilkeBox>
      {value &&
        (multiple || !pending.length) &&
        (isArray(value) ? (
          (value as (string | FileModel)[]).map((file: FileModel | string, index) => (
            <SilkeFileItem
              key={index}
              file={file}
              onRemove={() => onChange((value as (string | FileModel)[]).filter((v) => v !== file))}
            />
          ))
        ) : (
          <SilkeFileItem file={value} onRemove={() => onChange(null)} style={fileItemStyle} />
        ))}
      {pending.map((p, key) => (
        <SilkeFileItem
          style={fileItemStyle}
          key={key}
          file={p}
          loading
          error={p.error}
          onRemove={() => {
            // Settings this to tell promise that it should not complete this item
            p.stopped = true;
            setPending(pending.filter((m) => m !== p));
          }}
        />
      ))}
    </SilkeBox>
  );
}

type FileItemProps = {
  loading?: boolean;
  error?: string;
  file: string | FileModel | PendingFile;
  onRemove: () => void;
  style?: React.CSSProperties;
};

function SilkeFileItem({ file, error, loading, onRemove, style }: FileItemProps) {
  const ref = useRef<HTMLDivElement>(null);
  const [hover, setHover] = useState(false);
  if (isString(file)) {
    file = { name: last(file.split('/')) || 'Untitled', size: 0, type: 'test', url: file };
  }

  let cl = styles.item;
  if (error) cl += ' ' + styles.error;
  if (!error && !loading) cl += ' ' + styles.complete;

  return (
    <SilkeBox
      ref={ref}
      style={style}
      className={cl}
      vAlign="center"
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
    >
      <SilkeText overflow flex>
        {file.name}
      </SilkeText>
      <SilkeButton icon="close" kind="ghost" onClick={onRemove} />
      {!error && loading && <SilkeSkeleton className={styles.loader} />}
      {error && (
        <SilkeTooltip anchor={ref} show={hover} size="tiny" anchorOrigin="top-center">
          <SilkeText size="small">{error}</SilkeText>
        </SilkeTooltip>
      )}
    </SilkeBox>
  );
}
