import { Fun, Obj, Strings, Type, Unicode } from '@ephox/katamari';

import TextSeeker from 'tinymce/core/api/dom/TextSeeker';
import type Editor from 'tinymce/core/api/Editor';

import * as Options from '../api/Options';

import { findChar, freefallRtl, hasProtocol, isBracketOrSpace, isPunctuation } from './Utils';

interface ParseResult {
  readonly rng: Range;
  readonly url: string;
}

const parseCurrentLine = (editor: Editor, offset: number): ParseResult | null => {
  const voidElements = editor.schema.getVoidElements();
  const autoLinkPattern = Options.getAutoLinkPattern(editor);
  const { dom, selection } = editor;

  // Never create a link when we are inside a link
  if (dom.getParent(selection.getNode(), 'a[href]') !== null || editor.mode.isReadOnly()) {
    return null;
  }

  const rng = selection.getRng();
  const textSeeker = TextSeeker(dom, (node) => {
    return dom.isBlock(node) || Obj.has(voidElements, node.nodeName.toLowerCase()) || dom.getContentEditable(node) === 'false' || dom.getParent(node, 'a[href]') !== null;
  });

  // Descend down the end container to find the text node
  const { container: endContainer, offset: endOffset } = freefallRtl(rng.endContainer, rng.endOffset);

  // Find the root container to use when walking
  const root = dom.getParent(endContainer, dom.isBlock) ?? dom.getRoot();

  // Move the selection backwards to the start of the potential URL to account for the pressed character
  // while also excluding the last full stop from a word like "www.site.com."
  const endSpot = textSeeker.backwards(endContainer, endOffset + offset, (node, offset) => {
    const text = node.data;
    const idx = findChar(text, offset, Fun.not(isBracketOrSpace));
    // Move forward one so the offset is after the found character unless the found char is a punctuation char
    return idx === -1 || isPunctuation(text[idx]) ? idx : idx + 1;
  }, root);

  if (!endSpot) {
    return null;
  }

  // Walk backwards until we find a boundary or a bracket/space
  let lastTextNode = endSpot.container;
  const startSpot = textSeeker.backwards(endSpot.container, endSpot.offset, (node, offset) => {
    lastTextNode = node;
    const idx = findChar(node.data, offset, isBracketOrSpace);
    // Move forward one so that the offset is after the bracket/space
    return idx === -1 ? idx : idx + 1;
  }, root);

  const newRng = dom.createRng();
  if (!startSpot) {
    newRng.setStart(lastTextNode, 0);
  } else {
    newRng.setStart(startSpot.container, startSpot.offset);
  }
  newRng.setEnd(endSpot.container, endSpot.offset);

  const rngText = Unicode.removeZwsp(newRng.toString());
  const matches = rngText.match(autoLinkPattern);
  if (matches) {
    let url = matches[0];
    if (Strings.startsWith(url, 'www.')) {
      const protocol = Options.getDefaultLinkProtocol(editor);
      url = protocol + '://' + url;
    } else if (Strings.contains(url, '@') && !hasProtocol(url)) {
      url = 'mailto:' + url;
    }

    return { rng: newRng, url };
  } else {
    return null;
  }
};

const convertToLink = (editor: Editor, result: ParseResult): void => {
  const { dom, selection } = editor;
  const { rng, url } = result;

  const bookmark = selection.getBookmark();
  selection.setRng(rng);

  // Needs to be a native createlink command since this is executed in a keypress event handler
  // so the pending character that is to be inserted needs to be inserted after the link. That will not
  // happen if we use the formatter create link version. Since we're using the native command
  // then we also need to ensure the exec command events are fired for backwards compatibility.
  const command = 'createlink';
  const args = { command, ui: false, value: url };
  const beforeExecEvent = editor.dispatch('BeforeExecCommand', args);
  if (!beforeExecEvent.isDefaultPrevented()) {
    editor.getDoc().execCommand(command, false, url);
    editor.dispatch('ExecCommand', args);

    const defaultLinkTarget = Options.getDefaultLinkTarget(editor);
    if (Type.isString(defaultLinkTarget)) {
      const anchor = selection.getNode();
      dom.setAttrib(anchor, 'target', defaultLinkTarget);

      // Ensure noopener is added for blank targets to prevent window opener attacks
      if (defaultLinkTarget === '_blank' && !Options.allowUnsafeLinkTarget(editor)) {
        dom.setAttrib(anchor, 'rel', 'noopener');
      }
    }
  }

  selection.moveToBookmark(bookmark);
  editor.nodeChanged();
};

const handleSpacebar = (editor: Editor): void => {
  const result = parseCurrentLine(editor, -1);
  if (Type.isNonNullable(result)) {
    convertToLink(editor, result);
  }
};

const handleBracket = handleSpacebar;

const handleEnter = (editor: Editor): void => {
  const result = parseCurrentLine(editor, 0);
  if (Type.isNonNullable(result)) {
    convertToLink(editor, result);
  }
};

const setup = (editor: Editor): void => {
  editor.on('keydown', (e) => {
    if (e.keyCode === 13 && !e.isDefaultPrevented()) {
      handleEnter(editor);
    }
  });

  editor.on('keyup', (e) => {
    if (e.keyCode === 32) {
      handleSpacebar(editor);
    // One of the closing bracket keys: ), ] or }
    } else if (e.keyCode === 48 && e.shiftKey || e.keyCode === 221) {
      handleBracket(editor);
    }
  });
};

export {
  setup
};
