/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 * @flow
 * @preventMunge
 * @emails oncall+draft_js
 */

'use strict';

declare var __DEV__: boolean;
import type { BlockMap } from "./BlockMap";
import type { DraftEditorModes } from "./DraftEditorModes";
import type { DraftEditorDefaultProps, DraftEditorProps } from "./DraftEditorProps";
import type { DraftScrollPosition } from "./DraftScrollPosition";
const DefaultDraftBlockRenderMap = require("./DefaultDraftBlockRenderMap");
const DefaultDraftInlineStyle = require("./DefaultDraftInlineStyle");
const DraftEditorCompositionHandler = require("./DraftEditorCompositionHandler");
const DraftEditorContents = require("./DraftEditorContents.react");
const DraftEditorDragHandler = require("./DraftEditorDragHandler");
const DraftEditorEditHandler = require("./DraftEditorEditHandler");
const flushControlled = require("./DraftEditorFlushControlled");
const DraftEditorPlaceholder = require("./DraftEditorPlaceholder.react");
const DraftEffects = require("./DraftEffects");
const EditorState = require("./EditorState");
const React = require("react");
const Scroll = require("fbjs/lib/Scroll");
const Style = require("fbjs/lib/Style");
const UserAgent = require("fbjs/lib/UserAgent");
const cx = require("fbjs/lib/cx");
const generateRandomKey = require("./generateRandomKey");
const getDefaultKeyBinding = require("./getDefaultKeyBinding");
const getScrollPosition = require("fbjs/lib/getScrollPosition");
const gkx = require("./gkx");
const invariant = require("fbjs/lib/invariant");
const isHTMLElement = require("./isHTMLElement");
const nullthrows = require("fbjs/lib/nullthrows");
const isIE = UserAgent.isBrowser('IE');

// IE does not support the `input` event on contentEditable, so we can't
// observe spellcheck behavior.
const allowSpellCheck = !isIE;

// Define a set of handler objects to correspond to each possible `mode`
// of editor behavior.
const handlerMap = {
  edit: DraftEditorEditHandler,
  composite: DraftEditorCompositionHandler,
  drag: DraftEditorDragHandler,
  cut: null,
  render: null
};
type State = {
  contentsKey: number,
  ...
};
let didInitODS = false;
class UpdateDraftEditorFlags extends React.Component<{
  editor: DraftEditor,
  editorState: EditorState,
  ...
}> {
  render(): React.Node {
    return null;
  }
  componentDidMount(): mixed {
    this._update();
  }
  componentDidUpdate(): mixed {
    this._update();
  }
  _update() {
    const editor = this.props.editor;
    /**
     * Sometimes a render triggers a 'focus' or other event, and that will
     * schedule a second render pass.
     * In order to make sure the second render pass gets the latest editor
     * state, we update it here.
     * Example:
     * render #1
     * +
     * |
     * | cWU -> Nothing ... latestEditorState = STALE_STATE :(
     * |
     * | render -> this.props.editorState = FRESH_STATE
     * | +         *and* set latestEditorState = FRESH_STATE
     *   |
     * | |
     * | +--> triggers 'focus' event, calling 'handleFocus' with latestEditorState
     * |                                                +
     * |                                                |
     * +>cdU -> latestEditorState = FRESH_STATE         | the 'handleFocus' call schedules render #2
     *                                                  | with latestEditorState, which is FRESH_STATE
     *                                                  |
     * render #2 <--------------------------------------+
     * +
     * |
     * | cwU -> nothing updates
     * |
     * | render -> this.props.editorState = FRESH_STATE which was passed in above
     * |
     * +>cdU fires and resets latestEditorState = FRESH_STATE
     * ---
     * Note that if we don't set latestEditorState in 'render' in the above
     * diagram, then STALE_STATE gets passed to render #2.
     */
    editor._latestEditorState = this.props.editorState;

    /**
     * The reason we set this 'blockSelectEvents' flag is that  IE will fire a
     * 'selectionChange' event when we programmatically change the selection,
     * meaning it would trigger a new select event while we are in the middle
     * of updating.
     * We found that the 'selection.addRange' was what triggered the stray
     * selectionchange event in IE.
     * To be clear - we have not been able to reproduce specific bugs related
     * to this stray selection event, but have recorded logs that some
     * conditions do cause it to get bumped into during editOnSelect.
     */
    editor._blockSelectEvents = true;
  }
}

/**
 * `DraftEditor` is the root editor component. It composes a `contentEditable`
 * div, and provides a wide variety of useful function props for managing the
 * state of the editor. See `DraftEditorProps` for details.
 */
class DraftEditor extends React.Component<DraftEditorProps, State> {
  static defaultProps: DraftEditorDefaultProps = {
    ariaDescribedBy: '{{editor_id_placeholder}}',
    blockRenderMap: DefaultDraftBlockRenderMap,
    blockRendererFn: function () {
      return null;
    },
    blockStyleFn: function () {
      return '';
    },
    keyBindingFn: getDefaultKeyBinding,
    readOnly: false,
    spellCheck: false,
    stripPastedStyles: false
  };
  _blockSelectEvents: boolean;
  _clipboard: ?BlockMap;
  _handler: ?Object;
  _dragCount: number;
  _internalDrag: boolean;
  _editorKey: string;
  _placeholderAccessibilityID: string;
  _latestEditorState: EditorState;
  _latestCommittedEditorState: EditorState;
  _pendingStateFromBeforeInput: void | EditorState;

  /**
   * Define proxies that can route events to the current handler.
   */
  _onBeforeInput: Function;
  _onBlur: Function;
  _onCharacterData: Function;
  _onCompositionEnd: Function;
  _onCompositionStart: Function;
  _onCopy: Function;
  _onCut: Function;
  _onDragEnd: Function;
  _onDragOver: Function;
  _onDragStart: Function;
  _onDrop: Function;
  _onInput: Function;
  _onFocus: Function;
  _onKeyDown: Function;
  _onKeyPress: Function;
  _onKeyUp: Function;
  _onMouseDown: Function;
  _onMouseUp: Function;
  _onPaste: Function;
  _onSelect: Function;
  editor: ?HTMLElement;
  editorContainer: ?HTMLElement;
  focus: () => void;
  blur: () => void;
  setMode: (mode: DraftEditorModes) => void;
  exitCurrentMode: () => void;
  restoreEditorDOM: (scrollPosition?: DraftScrollPosition) => void;
  setClipboard: (clipboard: ?BlockMap) => void;
  getClipboard: () => ?BlockMap;
  getEditorKey: () => string;
  update: (editorState: EditorState) => void;
  updateSelection: () => void;
  onDragEnter: () => void;
  onDragLeave: () => void;
  constructor(props: DraftEditorProps) {
    super(props);
    this._blockSelectEvents = false;
    this._clipboard = null;
    this._handler = null;
    this._dragCount = 0;
    this._editorKey = props.editorKey || generateRandomKey();
    this._placeholderAccessibilityID = 'placeholder-' + this._editorKey;
    this._latestEditorState = props.editorState;
    this._latestCommittedEditorState = props.editorState;
    this._onBeforeInput = this._buildHandler('onBeforeInput');
    this._onBlur = this._buildHandler('onBlur');
    this._onCharacterData = this._buildHandler('onCharacterData');
    this._onCompositionEnd = this._buildHandler('onCompositionEnd');
    this._onCompositionStart = this._buildHandler('onCompositionStart');
    this._onCopy = this._buildHandler('onCopy');
    this._onCut = this._buildHandler('onCut');
    this._onDragEnd = this._buildHandler('onDragEnd');
    this._onDragOver = this._buildHandler('onDragOver');
    this._onDragStart = this._buildHandler('onDragStart');
    this._onDrop = this._buildHandler('onDrop');
    this._onInput = this._buildHandler('onInput');
    this._onFocus = this._buildHandler('onFocus');
    this._onKeyDown = this._buildHandler('onKeyDown');
    this._onKeyPress = this._buildHandler('onKeyPress');
    this._onKeyUp = this._buildHandler('onKeyUp');
    this._onMouseDown = this._buildHandler('onMouseDown');
    this._onMouseUp = this._buildHandler('onMouseUp');
    this._onPaste = this._buildHandler('onPaste');
    this._onSelect = this._buildHandler('onSelect');
    this.getEditorKey = () => this._editorKey;
    if (__DEV__) {
      ['onDownArrow', 'onEscape', 'onLeftArrow', 'onRightArrow', 'onTab', 'onUpArrow'].forEach(propName => {
        if (props.hasOwnProperty(propName)) {
          // eslint-disable-next-line no-console
          console.warn(`Supplying an \`${propName}\` prop to \`DraftEditor\` has ` + 'been deprecated. If your handler needs access to the keyboard ' + 'event, supply a custom `keyBindingFn` prop that falls back to ' + 'the default one (eg. https://is.gd/wHKQ3W).');
        }
      });
    }

    // See `restoreEditorDOM()`.
    this.state = {
      contentsKey: 0
    };
  }

  /**
   * Build a method that will pass the event to the specified handler method.
   * This allows us to look up the correct handler function for the current
   * editor mode, if any has been specified.
   */
  _buildHandler(eventName: string): Function {
    // Wrap event handlers in `flushControlled`. In sync mode, this is
    // effectively a no-op. In async mode, this ensures all updates scheduled
    // inside the handler are flushed before React yields to the browser.
    return e => {
      if (!this.props.readOnly) {
        const method = this._handler && this._handler[eventName];
        if (method) {
          if (flushControlled) {
            flushControlled(() => method(this, e));
          } else {
            method(this, e);
          }
        }
      }
    };
  }
  _handleEditorContainerRef: (HTMLElement | null) => void = (node: HTMLElement | null): void => {
    this.editorContainer = node;
    // Instead of having a direct ref on the child, we'll grab it here.
    // This is safe as long as the rendered structure is static (which it is).
    // This lets the child support ref={props.editorRef} without merging refs.
    this.editor = node !== null ? (node: any).firstChild : null;
  };
  _showPlaceholder(): boolean {
    return !!this.props.placeholder && !this.props.editorState.isInCompositionMode() && !this.props.editorState.getCurrentContent().hasText();
  }
  _renderPlaceholder(): React.Node {
    if (this._showPlaceholder()) {
      const placeHolderProps = {
        text: nullthrows(this.props.placeholder),
        editorState: this.props.editorState,
        textAlignment: this.props.textAlignment,
        accessibilityID: this._placeholderAccessibilityID
      };

      /* $FlowFixMe[incompatible-type] (>=0.112.0 site=www,mobile) This comment
       * suppresses an error found when Flow v0.112 was deployed. To see the
       * error delete this comment and run Flow. */
      return <DraftEditorPlaceholder {...placeHolderProps} />;
    }
    return null;
  }

  /**
   * returns ariaDescribedBy prop with '{{editor_id_placeholder}}' replaced with
   * the DOM id of the placeholder (if it exists)
   * @returns aria-describedby attribute value
   */
  _renderARIADescribedBy(): ?string {
    const describedBy = this.props.ariaDescribedBy || '';
    const placeholderID = this._showPlaceholder() ? this._placeholderAccessibilityID : '';
    return describedBy.replace('{{editor_id_placeholder}}', placeholderID) || undefined;
  }
  render(): React.Node {
    const {
      blockRenderMap,
      blockRendererFn,
      blockStyleFn,
      customStyleFn,
      customStyleMap,
      editorState,
      preventScroll,
      readOnly,
      textAlignment,
      textDirectionality
    } = this.props;
    const rootClass = cx({
      'DraftEditor/root': true,
      'DraftEditor/alignLeft': textAlignment === 'left',
      'DraftEditor/alignRight': textAlignment === 'right',
      'DraftEditor/alignCenter': textAlignment === 'center'
    });
    const contentStyle = {
      outline: 'none',
      // fix parent-draggable Safari bug. #1326
      userSelect: 'text',
      WebkitUserSelect: 'text',
      whiteSpace: 'pre-wrap',
      wordWrap: 'break-word'
    };

    // The aria-expanded and aria-haspopup properties should only be rendered
    // for a combobox.
    /* $FlowFixMe[prop-missing] (>=0.68.0 site=www,mobile) This comment
     * suppresses an error found when Flow v0.68 was deployed. To see the error
     * delete this comment and run Flow. */
    const ariaRole = this.props.role || 'textbox';
    const ariaExpanded = ariaRole === 'combobox' ? !!this.props.ariaExpanded : null;
    const editorContentsProps = {
      blockRenderMap,
      blockRendererFn,
      blockStyleFn,
      customStyleMap: {
        ...DefaultDraftInlineStyle,
        ...customStyleMap
      },
      customStyleFn,
      editorKey: this._editorKey,
      editorState,
      preventScroll,
      textDirectionality
    };
    return <div className={rootClass}>
        {this._renderPlaceholder()}
        <div className={cx('DraftEditor/editorContainer')} ref={this._handleEditorContainerRef}>
          {/* Note: _handleEditorContainerRef assumes this div won't move: */}
          <div id={this.props.id} aria-activedescendant={readOnly ? null : this.props.ariaActiveDescendantID} aria-autocomplete={readOnly ? null : this.props.ariaAutoComplete} aria-controls={readOnly ? null : this.props.ariaControls} aria-describedby={this._renderARIADescribedBy()} aria-expanded={readOnly ? null : ariaExpanded} aria-label={this.props.ariaLabel} aria-labelledby={this.props.ariaLabelledBy} aria-multiline={this.props.ariaMultiline} aria-owns={readOnly ? null : this.props.ariaOwneeID} autoCapitalize={this.props.autoCapitalize} autoComplete={this.props.autoComplete} autoCorrect={this.props.autoCorrect} className={cx({
          // Chrome's built-in translation feature mutates the DOM in ways
          // that Draft doesn't expect (ex: adding <font> tags inside
          // DraftEditorLeaf spans) and causes problems. We add notranslate
          // here which makes its autotranslation skip over this subtree.
          notranslate: !readOnly,
          'public/DraftEditor/content': true
        })} contentEditable={!readOnly} data-testid={this.props.webDriverTestID} onBeforeInput={this._onBeforeInput} onBlur={this._onBlur} onCompositionEnd={this._onCompositionEnd} onCompositionStart={this._onCompositionStart} onCopy={this._onCopy} onCut={this._onCut} onDragEnd={this._onDragEnd} onDragEnter={this.onDragEnter} onDragLeave={this.onDragLeave} onDragOver={this._onDragOver} onDragStart={this._onDragStart} onDrop={this._onDrop} onFocus={this._onFocus} onInput={this._onInput} onKeyDown={this._onKeyDown} onKeyPress={this._onKeyPress} onKeyUp={this._onKeyUp} onMouseDown={this._onMouseDown} onMouseUp={this._onMouseUp} onPaste={this._onPaste} onSelect={this._onSelect} ref={this.props.editorRef} role={readOnly ? null : ariaRole} spellCheck={allowSpellCheck && this.props.spellCheck} style={contentStyle} suppressContentEditableWarning tabIndex={this.props.tabIndex}>
            {/*
              Needs to come earlier in the tree as a sibling (not ancestor) of
              all DraftEditorLeaf nodes so it's first in postorder traversal.
             */}
            <UpdateDraftEditorFlags editor={this} editorState={editorState} />
            <DraftEditorContents {...editorContentsProps} key={'contents' + this.state.contentsKey} />
          </div>
        </div>
      </div>;
  }
  componentDidMount(): void {
    this._blockSelectEvents = false;
    if (!didInitODS && gkx('draft_ods_enabled')) {
      didInitODS = true;
      DraftEffects.initODS();
    }
    this.setMode('edit');

    /**
     * IE has a hardcoded "feature" that attempts to convert link text into
     * anchors in contentEditable DOM. This breaks the editor's expectations of
     * the DOM, and control is lost. Disable it to make IE behave.
     * See: http://blogs.msdn.com/b/ieinternals/archive/2010/09/15/
     * ie9-beta-minor-change-list.aspx
     */
    if (isIE) {
      // editor can be null after mounting
      // https://stackoverflow.com/questions/44074747/componentdidmount-called-before-ref-callback
      if (!this.editor) {
        global.execCommand('AutoUrlDetect', false, false);
      } else {
        this.editor.ownerDocument.execCommand('AutoUrlDetect', false, false);
      }
    }
  }
  componentDidUpdate(): void {
    this._blockSelectEvents = false;
    this._latestEditorState = this.props.editorState;
    this._latestCommittedEditorState = this.props.editorState;
  }

  /**
   * Used via `this.focus()`.
   *
   * Force focus back onto the editor node.
   *
   * We attempt to preserve scroll position when focusing. You can also pass
   * a specified scroll position (for cases like `cut` behavior where it should
   * be restored to a known position).
   */
  focus: (scrollPosition?: DraftScrollPosition) => void = (scrollPosition?: DraftScrollPosition): void => {
    const {
      editorState
    } = this.props;
    const alreadyHasFocus = editorState.getSelection().getHasFocus();
    const editorNode = this.editor;
    if (!editorNode) {
      // once in a while people call 'focus' in a setTimeout, and the node has
      // been deleted, so it can be null in that case.
      return;
    }
    const scrollParent = Style.getScrollParent(editorNode);
    const {
      x,
      y
    } = scrollPosition || getScrollPosition(scrollParent);
    invariant(isHTMLElement(editorNode), 'editorNode is not an HTMLElement');
    editorNode.focus();

    // Restore scroll position
    if (scrollParent === window) {
      window.scrollTo(x, y);
    } else {
      Scroll.setTop(scrollParent, y);
    }

    // On Chrome and Safari, calling focus on contenteditable focuses the
    // cursor at the first character. This is something you don't expect when
    // you're clicking on an input element but not directly on a character.
    // Put the cursor back where it was before the blur.
    if (!alreadyHasFocus) {
      this.update(EditorState.forceSelection(editorState, editorState.getSelection()));
    }
  };
  blur: () => void = (): void => {
    const editorNode = this.editor;
    if (!editorNode) {
      return;
    }
    invariant(isHTMLElement(editorNode), 'editorNode is not an HTMLElement');
    editorNode.blur();
  };

  /**
   * Used via `this.setMode(...)`.
   *
   * Set the behavior mode for the editor component. This switches the current
   * handler module to ensure that DOM events are managed appropriately for
   * the active mode.
   */
  setMode: (DraftEditorModes) => void = (mode: DraftEditorModes): void => {
    const {
      onPaste,
      onCut,
      onCopy
    } = this.props;
    const editHandler = {
      ...handlerMap.edit
    };
    if (onPaste) {
      /* $FlowFixMe[incompatible-type] (>=0.117.0 site=www,mobile) This comment
       * suppresses an error found when Flow v0.117 was deployed. To see the
       * error delete this comment and run Flow. */
      editHandler.onPaste = onPaste;
    }
    if (onCut) {
      editHandler.onCut = onCut;
    }
    if (onCopy) {
      editHandler.onCopy = onCopy;
    }
    const handler = {
      ...handlerMap,
      edit: editHandler
    };
    this._handler = handler[mode];
  };
  exitCurrentMode: () => void = (): void => {
    this.setMode('edit');
  };

  /**
   * Used via `this.updateSelection()`.
   *
   * Updates the editor state's selection based on the current DOM selection
   * Use when (before) updating the editor state while mouse selection is in progress to prevent interrupting it
   */
  updateSelection: () => void = (): void => {
    this._onSelect(new Event('select', {
      bubbles: true,
      cancelable: true
    }));
  };

  /**
   * Used via `this.restoreEditorDOM()`.
   *
   * Force a complete re-render of the DraftEditorContents based on the current
   * EditorState. This is useful when we know we are going to lose control of
   * the DOM state (cut command, IME) and we want to make sure that
   * reconciliation occurs on a version of the DOM that is synchronized with
   * our EditorState.
   */
  restoreEditorDOM: (scrollPosition?: DraftScrollPosition) => void = (scrollPosition?: DraftScrollPosition): void => {
    this.setState({
      contentsKey: this.state.contentsKey + 1
    }, () => {
      this.focus(scrollPosition);
    });
  };

  /**
   * Used via `this.setClipboard(...)`.
   *
   * Set the clipboard state for a cut/copy event.
   */
  setClipboard: (?BlockMap) => void = (clipboard: ?BlockMap): void => {
    this._clipboard = clipboard;
  };

  /**
   * Used via `this.getClipboard()`.
   *
   * Retrieve the clipboard state for a cut/copy event.
   */
  getClipboard: () => ?BlockMap = (): ?BlockMap => {
    return this._clipboard;
  };

  /**
   * Used via `this.update(...)`.
   *
   * Propagate a new `EditorState` object to higher-level components. This is
   * the method by which event handlers inform the `DraftEditor` component of
   * state changes. A component that composes a `DraftEditor` **must** provide
   * an `onChange` prop to receive state updates passed along from this
   * function.
   */
  update: (EditorState) => void = (editorState: EditorState): void => {
    this._latestEditorState = editorState;
    this.props.onChange(editorState);
  };

  /**
   * Used in conjunction with `onDragLeave()`, by counting the number of times
   * a dragged element enters and leaves the editor (or any of its children),
   * to determine when the dragged element absolutely leaves the editor.
   */
  onDragEnter: () => void = (): void => {
    this._dragCount++;
  };

  /**
   * See `onDragEnter()`.
   */
  onDragLeave: () => void = (): void => {
    this._dragCount--;
    if (this._dragCount === 0) {
      this.exitCurrentMode();
    }
  };
}
module.exports = DraftEditor;