import * as React from 'react';

import { HistoryNavigator } from '../common';
import {
  formatWithPattern,
  getCountOfSymbolsInSelection,
  getCursorPositionAfterKeystroke,
  unformatWithPattern,
  getDistanceToPreviousSymbol,
  getDistanceToNextSymbol,
} from '../common/textFormat';
import { InputProps } from '../inputs/Input';
import { TextAreaProps } from '../inputs/TextArea';

type HTMLTextElement = HTMLInputElement | HTMLTextAreaElement;
type TextElementProps = InputProps | TextAreaProps;

export type EventType =
  | 'KeyDown'
  | 'Paste'
  | 'Cut'
  | 'Undo'
  | 'Redo'
  | 'Backspace'
  | 'Delete'
  | 'Initial';

interface WithDisplayFormatState {
  value: string;
  historyNavigator: HistoryNavigator;
  prevDisplayPattern: string;
  triggerType: EventType;
  triggerEvent: React.KeyboardEvent<HTMLTextElement> | null;
  pastedLength: number;
  selectionStart: number;
  selectionEnd: number;
}

export interface WithDisplayFormatProps<T extends TextElementProps = TextElementProps> extends Pick<
  TextElementProps,
  | 'className'
  | 'disabled'
  | 'id'
  | 'maxLength'
  | 'minLength'
  | 'name'
  | 'placeholder'
  | 'readOnly'
  | 'required'
  | 'inputMode'
> {
  /** @default '' */
  value?: string;
  /** @default '' */
  displayPattern: string;
  /**
   * autocomplete hides our form help so we need to disable it when help text
   * is present. Chrome ignores autocomplete=off, the only way to disable it is
   * to provide an 'invalid' value, for which 'disabled' serves.
   * @default 'off'
   */
  autoComplete?: TextElementProps['autoComplete'] | 'disabled';
  onChange?: (value: string) => void;
  onBlur?: (value: string) => void;
  onFocus?: (value: string) => void;
  render: (renderProps: T) => React.JSX.Element;
}

class WithDisplayFormat<T extends TextElementProps> extends React.Component<
  WithDisplayFormatProps<T>,
  WithDisplayFormatState
> {
  declare props: WithDisplayFormatProps<T> &
    Required<Pick<WithDisplayFormatProps<T>, keyof typeof WithDisplayFormat.defaultProps>>;
  static defaultProps = {
    autoComplete: 'off',
    displayPattern: '',
    value: '',
  };

  constructor(props: WithDisplayFormatProps) {
    super(props);
    const unformattedValue = unformatWithPattern(props.value ?? '', props.displayPattern);
    this.state = {
      value: formatWithPattern(unformattedValue, props.displayPattern),
      historyNavigator: new HistoryNavigator(),
      prevDisplayPattern: props.displayPattern,
      triggerType: 'Initial',
      triggerEvent: null,
      selectionStart: 0,
      selectionEnd: 0,
      pastedLength: 0,
    };
  }

  static getDerivedStateFromProps(
    { displayPattern }: WithDisplayFormatProps,
    { prevDisplayPattern = displayPattern, value, historyNavigator }: WithDisplayFormatState,
  ) {
    if (prevDisplayPattern !== displayPattern) {
      const unFormattedValue = unformatWithPattern(value, prevDisplayPattern);
      historyNavigator.reset();

      return {
        prevDisplayPattern: displayPattern,
        value: formatWithPattern(unFormattedValue, displayPattern),
        triggerType: null,
        triggerEvent: null,
        pastedLength: 0,
      };
    }
    return null;
  }

  getUserAction = (unformattedValue: string): EventType | string => {
    const { triggerEvent, triggerType, value } = this.state;
    const { displayPattern } = this.props;

    if (triggerEvent) {
      if (triggerType === 'Paste' || triggerType === 'Cut') {
        return triggerType;
      }

      if ((triggerEvent.ctrlKey || triggerEvent.metaKey) && triggerEvent.key === 'z') {
        return triggerEvent.shiftKey ? 'Redo' : 'Undo';
      }

      // Detect mouse event redo
      if (triggerEvent.ctrlKey && triggerEvent.key === 'd') {
        return 'Delete';
      }

      // Android Fix.
      if (
        typeof triggerEvent.key === 'undefined' &&
        unformattedValue.length <= unformatWithPattern(value, displayPattern).length
      ) {
        return 'Backspace';
      }
      return triggerEvent.key;
    }
    // triggerEvent can be null only in case of "autofilling" (via password manager extension or browser build-in one) events
    return 'Paste';
  };

  resetEvent = () => {
    this.setState({
      triggerType: 'Initial',
      triggerEvent: null,
      pastedLength: 0,
    });
  };

  detectUndoRedo = (event: React.KeyboardEvent<HTMLTextElement>) => {
    if ((event.ctrlKey || event.metaKey) && event.key === 'z') {
      return event.shiftKey ? 'Redo' : 'Undo';
    }
    return null;
  };

  handleOnKeyDown: React.KeyboardEventHandler<HTMLTextElement> = (event) => {
    event.persist();
    const { selectionStart, selectionEnd } = event.currentTarget;
    const { historyNavigator } = this.state;
    const { displayPattern } = this.props;

    // Unfortunately Undo and Redo don't trigger OnChange event so we need to handle some value logic here.
    let newFormattedValue = '';

    if (this.detectUndoRedo(event) === 'Undo') {
      newFormattedValue = formatWithPattern(historyNavigator.undo().toString(), displayPattern);
      this.setState({ value: newFormattedValue, triggerType: 'Undo' });
    } else if (this.detectUndoRedo(event) === 'Redo') {
      newFormattedValue = formatWithPattern(historyNavigator.redo().toString(), displayPattern);
      this.setState({ value: newFormattedValue, triggerType: 'Redo' });
    } else {
      this.setState({
        triggerEvent: event,
        triggerType: 'KeyDown',
        selectionStart: selectionStart ?? 0,
        selectionEnd: selectionEnd ?? 0,
      });
    }
  };

  handleOnPaste: React.ClipboardEventHandler<HTMLTextElement> = (event) => {
    const { displayPattern } = this.props;
    const pastedLength = unformatWithPattern(
      event.clipboardData.getData('Text'),
      displayPattern,
    ).length;

    this.setState({ triggerType: 'Paste', pastedLength });
  };

  handleOnCut: React.ClipboardEventHandler<HTMLTextElement> = () => {
    this.setState({ triggerType: 'Cut' });
  };

  isKeyAllowed = (action: EventType | string) => {
    const { displayPattern } = this.props;
    const symbolsInPattern = displayPattern.split('').filter((character) => character !== '*');

    return !symbolsInPattern.includes(action);
  };

  handleOnChange: React.ChangeEventHandler<HTMLTextElement> = (event) => {
    const { historyNavigator, triggerType } = this.state;
    const { displayPattern, onChange } = this.props;
    const { value } = event.target;
    let unformattedValue = unformatWithPattern(value, displayPattern);
    const action = this.getUserAction(unformattedValue);
    if (!this.isKeyAllowed(action) || triggerType === 'Undo' || triggerType === 'Redo') {
      return;
    }

    if (action === 'Backspace' || action === 'Delete') {
      unformattedValue = this.handleDelete(unformattedValue, action);
    }

    const newFormattedValue = formatWithPattern(unformattedValue, displayPattern);
    historyNavigator.add(unformattedValue);

    this.handleCursorPositioning(action);

    this.setState({ value: newFormattedValue }, () => {
      this.resetEvent();
      if (onChange) {
        const broadcastValue = unformatWithPattern(newFormattedValue, displayPattern);
        onChange(broadcastValue);
      }
    });
  };

  handleOnBlur: React.FocusEventHandler<HTMLTextElement> = (event) => {
    this.props.onBlur?.(unformatWithPattern(event.target.value, this.props.displayPattern));
  };

  handleOnFocus: React.FocusEventHandler<HTMLTextElement> = (event) => {
    const { displayPattern, onFocus } = this.props;
    if (onFocus) {
      onFocus(unformatWithPattern(event.target.value, displayPattern));
    }
  };

  handleDelete = (unformattedValue: string, action: EventType) => {
    const { displayPattern } = this.props;
    const { selectionStart, selectionEnd } = this.state;
    const newStack = [...unformattedValue];
    if (selectionStart === selectionEnd) {
      let startPosition =
        selectionStart - getCountOfSymbolsInSelection(0, selectionStart, displayPattern);
      let toDelete = 0;

      let count = getDistanceToNextSymbol(selectionStart, displayPattern);
      if (action === 'Backspace') {
        startPosition -= 1;
        count = getDistanceToPreviousSymbol(selectionStart, displayPattern);
      }

      if (startPosition >= 0 && count) {
        toDelete = 1;
      }

      newStack.splice(startPosition, toDelete);
    }

    return newStack.join('');
  };

  handleCursorPositioning = (action: string) => {
    const { displayPattern } = this.props;
    const { triggerEvent, selectionStart, selectionEnd, pastedLength } = this.state;

    const cursorPosition = getCursorPositionAfterKeystroke(
      action,
      selectionStart,
      selectionEnd,
      displayPattern,
      pastedLength,
    );

    setTimeout(() => {
      if (triggerEvent) {
        (triggerEvent.target as HTMLTextElement).setSelectionRange(cursorPosition, cursorPosition);
      }
      this.setState({ selectionStart: cursorPosition, selectionEnd: cursorPosition });
    }, 0);
  };

  render() {
    const {
      inputMode,
      className,
      id,
      name,
      placeholder,
      readOnly,
      required,
      minLength,
      maxLength,
      disabled,
      autoComplete,
    } = this.props;
    const { value } = this.state;
    const renderProps: TextElementProps = {
      inputMode,
      className,
      id,
      name,
      placeholder,
      readOnly,
      required,
      minLength,
      maxLength,
      disabled,
      autoComplete,
      value,
      onFocus: this.handleOnFocus,
      onBlur: this.handleOnBlur,
      onPaste: this.handleOnPaste,
      onKeyDown: this.handleOnKeyDown,
      onChange: this.handleOnChange,
      onCut: this.handleOnCut,
    };
    return this.props.render(renderProps as T);
  }
}

export default WithDisplayFormat;
