import { Fun } from '@ephox/katamari';
import { PlatformDetection } from '@ephox/sand';
import { Class, Focus, SugarElement, SugarShadowDom, WindowVisualViewport } from '@ephox/sugar';

import DOMUtils from '../api/dom/DOMUtils';
import Editor from '../api/Editor';
import EditorManager from '../api/EditorManager';
import FocusManager from '../api/FocusManager';
import * as Options from '../api/Options';
import Delay from '../api/util/Delay';
import * as NodeType from '../dom/NodeType';
import * as OuterPosition from '../frames/OuterPosition';
import * as SelectionRestore from '../selection/SelectionRestore';

let documentFocusInHandler: ((e: FocusEvent) => void) | null;
const DOM = DOMUtils.DOM;

const isEditorUIElement = (elm: Node): boolean => {
  // Since this can be overridden by third party we need to use the API reference here
  return NodeType.isElement(elm) && FocusManager.isEditorUIElement(elm);
};

const isEditorContentAreaElement = (elm: Element): boolean => {
  const classList = elm.classList;
  if (classList !== undefined) {
    // tox-edit-area__iframe === iframe container element
    // mce-content-body === inline body element
    return classList.contains('tox-edit-area') || classList.contains('tox-edit-area__iframe') || classList.contains('mce-content-body');
  } else {
    return false;
  }
};

const isUIElement = (editor: Editor, elm: Node): boolean => {
  const customSelector = Options.getCustomUiSelector(editor);
  const parent = DOM.getParent(elm, (elm) => {
    return (
      isEditorUIElement(elm) ||
      (customSelector ? editor.dom.is(elm, customSelector) : false)
    );
  });
  return parent !== null;
};

const getActiveElement = (editor: Editor): Element => {
  try {
    const root = SugarShadowDom.getRootNode(SugarElement.fromDom(editor.getElement()));
    return Focus.active(root).fold(
      () => document.body,
      (x) => x.dom
    );
  } catch {
    // IE sometimes fails to get the activeElement when resizing table
    // TODO: Investigate this
    return document.body;
  }
};

const registerEvents = (editorManager: EditorManager, e: { editor: Editor }) => {
  const editor = e.editor;

  SelectionRestore.register(editor);

  const toggleContentAreaOnFocus = (editor: Editor, fn: (element: SugarElement<Element>, clazz: string) => void) => {
    // Inline editors have a different approach to highlight the content area on focus
    if (Options.shouldHighlightOnFocus(editor) && editor.inline !== true) {
      const contentArea = SugarElement.fromDom(editor.getContainer());
      fn(contentArea, 'tox-edit-focus');
    }
  };

  const bringEditorIntoView = (editor: Editor) => {
    const minimumVisibility = 25;
    if (!editor.iframeElement) {
      return;
    }
    const element = SugarElement.fromDom(editor.iframeElement);
    const op = OuterPosition.find(element);
    const viewportBounds = WindowVisualViewport.getBounds(window);
    if (op.top < viewportBounds.y || op.top > (viewportBounds.bottom - minimumVisibility)) {
      element.dom.scrollIntoView({ block: 'center' });
    }
  };

  editor.on('focusin', () => {
    const focusedEditor = editorManager.focusedEditor;

    if (isEditorContentAreaElement(getActiveElement(editor))) {
      toggleContentAreaOnFocus(editor, Class.add);
    }

    if (focusedEditor !== editor) {
      if (focusedEditor) {
        focusedEditor.dispatch('blur', { focusedEditor: editor });
      }

      editorManager.setActive(editor);
      editorManager.focusedEditor = editor;
      editor.dispatch('focus', { blurredEditor: focusedEditor });
      editor.focus(true);
      const browser = PlatformDetection.detect().browser;
      if (editor.inline !== true && (browser.isSafari() || browser.isChromium())) {
        bringEditorIntoView(editor);
      }
    }
  });

  editor.on('focusout', () => {
    Delay.setEditorTimeout(editor, () => {
      const focusedEditor = editorManager.focusedEditor;

      // Remove focus highlight when the content area is no longer the active editor element, or if the highlighted editor is not the current focused editor
      if (!isEditorContentAreaElement(getActiveElement(editor)) || focusedEditor !== editor) {
        toggleContentAreaOnFocus(editor, Class.remove);
      }

      // Still the same editor the blur was outside any editor UI
      if (!isUIElement(editor, getActiveElement(editor)) && focusedEditor === editor) {
        editor.dispatch('blur', { focusedEditor: null });
        editorManager.focusedEditor = null;
      }
    });
  });

  // Check if focus is moved to an element outside the active editor by checking if the target node
  // isn't within the body of the activeEditor nor a UI element such as a dialog child control
  if (!documentFocusInHandler) {
    documentFocusInHandler = (e: FocusEvent) => {
      const activeEditor = editorManager.activeEditor;

      if (activeEditor) {
        SugarShadowDom.getOriginalEventTarget(e).each((target) => {
          const elem = (target as Node);
          if (elem.ownerDocument === document) {
            // Fire a blur event if the element isn't a UI element
            if (elem !== document.body && !isUIElement(activeEditor, elem) && editorManager.focusedEditor === activeEditor) {
              activeEditor.dispatch('blur', { focusedEditor: null });
              editorManager.focusedEditor = null;
            }
          }
        });
      }
    };

    DOM.bind(document, 'focusin', documentFocusInHandler);
  }
};

const unregisterDocumentEvents = (editorManager: EditorManager, e: { editor: Editor }) => {
  if (editorManager.focusedEditor === e.editor) {
    editorManager.focusedEditor = null;
  }

  if (!editorManager.activeEditor && documentFocusInHandler) {
    DOM.unbind(document, 'focusin', documentFocusInHandler);
    documentFocusInHandler = null;
  }
};

const setup = (editorManager: EditorManager): void => {
  editorManager.on('AddEditor', Fun.curry(registerEvents, editorManager));
  editorManager.on('RemoveEditor', Fun.curry(unregisterDocumentEvents, editorManager));
};

export {
  setup,
  isEditorUIElement,
  isEditorContentAreaElement,
  isUIElement
};
