/**
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Copyright (c) KALEIDOS INC
 */

import { createLineBreak, isLineBreak } from "../content/dom/LineBreak.js";
import {
  createInline,
  createInlineFrom,
  getInline,
  getInlineLength,
  isInline,
  isInlineStart,
  isInlineEnd,
  setInlineStyles,
  mergeInlines,
  splitInline,
  createEmptyInline,
} from "../content/dom/Inline.js";
import {
  createEmptyParagraph,
  isEmptyParagraph,
  getParagraph,
  isParagraph,
  isParagraphStart,
  isParagraphEnd,
  setParagraphStyles,
  splitParagraph,
  splitParagraphAtNode,
  mergeParagraphs,
  fixParagraph,
} from "../content/dom/Paragraph.js";
import {
  removeBackward,
  removeForward,
  replaceWith,
  insertInto,
  removeSlice,
} from "../content/Text.js";
import { getTextNodeLength, getClosestTextNode, isTextNode } from "../content/dom/TextNode.js";
import TextNodeIterator from "../content/dom/TextNodeIterator.js";
import TextEditor from "../TextEditor.js";
import CommandMutations from "../commands/CommandMutations.js";
import { setRootStyles } from "../content/dom/Root.js";
import { SelectionDirection } from "./SelectionDirection.js";
import SafeGuard from "./SafeGuard.js";

const SAFE_GUARD = true;
const SAFE_GUARD_TIME = true;

/**
 * Supported options for the SelectionController.
 *
 * @typedef {Object} SelectionControllerOptions
 * @property {Object} [debug] An object with references to DOM elements that will keep all the debugging values.
 */

/**
 * SelectionController uses the same concepts used by the Selection API but extending it to support
 * our own internal model based on paragraphs (in drafconst textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
      createParagraph([createInline(new Text("Hello, "))]),
      createEmptyParagraph(),
      createParagraph([createInline(new Text("World!"))]),
    ]);
    const root = textEditorMock.root;
    const selection = document.getSelection();
    const selectionController = new SelectionController(
      textEditorMock,
      selection
    );
    focus(
      selection,
      textEditorMock,
      root.childNodes.item(2).firstChild.firstChild,
      0
    );
    selectionController.mergeBackwardParagraph();
    expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.children.length).toBe(2);
    expect(textEditorMock.root.dataset.itype).toBe("root");
    expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
    expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph");
    expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf(
      HTMLSpanElement
    );
    expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
      "inline"
    );
    expect(textEditorMock.root.textContent).toBe("Hello, World!");
    expect(textEditorMock.root.firstChild.textContent).toBe("Hello, ");
    expect(textEditorMock.root.lastChild.textContent).toBe("World!");
  t.js they were called blocks) and inlines.
 */
export class SelectionController extends EventTarget {
  /**
   * Reference to the text editor.
   *
   * @type {TextEditor}
   */
  #textEditor = null;

  /**
   * Selection.
   *
   * @type {Selection}
   */
  #selection = null;

  /**
   * Set of ranges (this should always have one)
   *
   * @type {Set<Range>}
   */
  #ranges = new Set();

  /**
   * Current range (.rangeAt 0)
   *
   * @type {Range}
   */
  #range = null;

  /**
   * @type {Node}
   */
  #focusNode = null;

  /**
   * @type {number}
   */
  #focusOffset = 0;

  /**
   * @type {Node}
   */
  #anchorNode = null;

  /**
   * @type {number}
   */
  #anchorOffset = 0;

  /**
   * Saved selection.
   *
   * @type {object}
   */
  #savedSelection = null;

  /**
   * TextNodeIterator that allows us to move
   * around the root element but only through
   * <br> and #text nodes.
   *
   * @type {TextNodeIterator}
   */
  #textNodeIterator = null;

  /**
   * CSSStyleDeclaration that we can mutate
   * to handle style changes.
   *
   * @type {CSSStyleDeclaration}
   */
  #currentStyle = null;

  /**
   * Element used to have a custom CSSStyleDeclaration
   * that we can modify to handle style changes when the
   * selection is changed.
   *
   * @type {HTMLDivElement}
   */
  #inertElement = null;

  /**
   * @type {SelectionControllerDebug}
   */
  #debug = null;

  /**
   * Command Mutations.
   *
   * @type {CommandMutations}
   */
  #mutations = new CommandMutations();

  /**
   * Style defaults.
   *
   * @type {Object.<string, *>}
   */
  #styleDefaults = null;

  /**
   * Fix for Chrome.
   */
  #fixInsertCompositionText = false;

  /**
   * Constructor
   *
   * @param {TextEditor} textEditor
   * @param {Selection} selection
   * @param {SelectionControllerOptions} [options]
   */
  constructor(textEditor, selection, options) {
    super();
    // FIXME: We can't check if it is an instanceof TextEditor
    //        because tests use TextEditorMock.
    /*
    if (!(textEditor instanceof TextEditor)) {
      throw new TypeError("Invalid EventTarget");
    }
    */
    this.#debug = options?.debug;
    this.#styleDefaults = options?.styleDefaults;
    this.#selection = selection;
    this.#textEditor = textEditor;
    this.#textNodeIterator = new TextNodeIterator(this.#textEditor.element);

    // Setups everything.
    this.#setup();
  }

  /**
   * Styles of the current inline.
   *
   * @type {CSSStyleDeclaration}
   */
  get currentStyle() {
    return this.#currentStyle;
  }

  /**
   * Applies the default styles to the currentStyle
   * CSSStyleDeclaration.
   */
  #applyDefaultStylesToCurrentStyle() {
    if (this.#styleDefaults) {
      for (const [name, value] of Object.entries(this.#styleDefaults)) {
        this.#currentStyle.setProperty(
          name,
          value + (name === "font-size" ? "px" : "")
        );
      }
    }
  }

  /**
   * Applies some styles to the currentStyle
   * CSSStyleDeclaration
   *
   * @param {HTMLElement} element
   */
  #applyStylesToCurrentStyle(element) {
    for (let index = 0; index < element.style.length; index++) {
      const styleName = element.style.item(index);
      const styleValue = element.style.getPropertyValue(styleName);
      this.#currentStyle.setProperty(styleName, styleValue);
    }
  }

  /**
   * Updates current styles based on the currently selected inline.
   *
   * @param {HTMLSpanElement} inline
   * @returns {SelectionController}
   */
  #updateCurrentStyle(inline) {
    this.#applyDefaultStylesToCurrentStyle();
    const root = inline.parentElement.parentElement;
    this.#applyStylesToCurrentStyle(root);
    const paragraph = inline.parentElement;
    this.#applyStylesToCurrentStyle(paragraph);
    this.#applyStylesToCurrentStyle(inline);
    return this;
  }

  /**
   * This is called on every `selectionchange` because it is dispatched
   * only by the `document` object.
   *
   * @param {Event} e
   */
  #onSelectionChange = (e) => {
    // If we're outside the contenteditable element, then
    // we return.
    if (!this.hasFocus) {
      return;
    }

    let focusNodeChanges = false;
    let anchorNodeChanges = false;

    if (this.#focusNode !== this.#selection.focusNode) {
      this.#focusNode = this.#selection.focusNode;
      focusNodeChanges = true;
    }
    this.#focusOffset = this.#selection.focusOffset;

    if (this.#anchorNode !== this.#selection.anchorNode) {
      this.#anchorNode = this.#selection.anchorNode;
      anchorNodeChanges = true;
    }
    this.#anchorOffset = this.#selection.anchorOffset;

    // We need to handle multi selection from firefox
    // and remove all the old ranges and just keep the
    // last one added.
    if (this.#selection.rangeCount > 1) {
      for (let index = 0; index < this.#selection.rangeCount; index++) {
        const range = this.#selection.getRangeAt(index);
        if (this.#ranges.has(range)) {
          this.#ranges.delete(range);
          this.#selection.removeRange(range);
        } else {
          this.#ranges.add(range);
          this.#range = range;
        }
      }
    } else if (this.#selection.rangeCount > 0) {
      const range = this.#selection.getRangeAt(0);
      this.#range = range;
      this.#ranges.clear();
      this.#ranges.add(range);
    } else {
      this.#range = null;
      this.#ranges.clear();
    }

    // If focus node changed, we need to retrieve all the
    // styles of the current inline and dispatch an event
    // to notify that the styles have changed.
    if (focusNodeChanges) {
      this.#notifyStyleChange();
    }

    if (this.#fixInsertCompositionText) {
      this.#fixInsertCompositionText = false;
      const lineBreak = fixParagraph(this.focusNode);
      this.collapse(lineBreak, 0);
    }

    if (this.#debug) {
      this.#debug.update(this);
    }
  };

  /**
   * Notifies that the styles have changed.
   */
  #notifyStyleChange() {
    const inline = this.focusInline;
    if (inline) {
      this.#updateCurrentStyle(inline);
      this.dispatchEvent(
        new CustomEvent("stylechange", {
          detail: this.#currentStyle,
        })
      );
    }
  }

  /**
   * Setups
   */
  #setup() {
    // This element is not attached to the DOM
    // so it doesn't trigger style or layout calculations.
    // That's why it's called "inertElement".
    this.#inertElement = document.createElement("div");
    this.#currentStyle = this.#inertElement.style;
    this.#applyDefaultStylesToCurrentStyle();

    if (this.#selection.rangeCount > 0) {
      const range = this.#selection.getRangeAt(0);
      this.#range = range;
      this.#ranges.add(range);
    }

    // If there are more than one range, we should remove
    // them because this is a feature not supported by browsers
    // like Safari and Chrome.
    if (this.#selection.rangeCount > 1) {
      for (let index = 1; index < this.#selection.rangeCount; index++) {
        this.#selection.removeRange(index);
      }
    }
    document.addEventListener("selectionchange", this.#onSelectionChange);
  }

  /**
   * Returns a Range-like object.
   *
   * @returns {RangeLike}
   */
  #getSavedRange() {
    if (!this.#range) {
      return {
        collapsed: true,
        commonAncestorContainer: null,
        startContainer: null,
        startOffset: 0,
        endContainer: null,
        endOffset: 0,
      };
    }
    return {
      collapsed: this.#range.collapsed,
      commonAncestorContainer: this.#range.commonAncestorContainer,
      startContainer: this.#range.startContainer,
      startOffset: this.#range.startOffset,
      endContainer: this.#range.endContainer,
      endOffset: this.#range.endOffset,
    };
  }

  /**
   * Saves the current selection and returns the client rects.
   *
   * @returns {boolean}
   */
  saveSelection() {
    this.#savedSelection = {
      isCollapsed: this.#selection.isCollapsed,
      focusNode: this.#selection.focusNode,
      focusOffset: this.#selection.focusOffset,
      anchorNode: this.#selection.anchorNode,
      anchorOffset: this.#selection.anchorOffset,
      range: this.#getSavedRange(),
    };
    return true;
  }

  /**
   * Restores a saved selection if there's any.
   *
   * @returns {boolean}
   */
  restoreSelection() {
    if (!this.#savedSelection) return false;

    if (this.#savedSelection.anchorNode && this.#savedSelection.focusNode) {
      if (this.#savedSelection.anchorNode === this.#savedSelection.focusNode) {
        this.#selection.setPosition(this.#savedSelection.focusNode, this.#savedSelection.focusOffset);
      } else {
        this.#selection.setBaseAndExtent(
          this.#savedSelection.anchorNode,
          this.#savedSelection.anchorOffset,
          this.#savedSelection.focusNode,
          this.#savedSelection.focusOffset
        );
      }
    }
    this.#savedSelection = null;
    return true;
  }

  /**
   * Marks the start of a mutation.
   *
   * Clears all the mutations kept in CommandMutations.
   */
  startMutation() {
    this.#mutations.clear();
    if (!this.#focusNode) return false;
    return true;
  }

  /**
   * Marks the end of a mutation.
   *
   * @returns
   */
  endMutation() {
    return this.#mutations;
  }

  /**
   * Selects all content.
   */
  selectAll() {
    this.#selection.selectAllChildren(this.#textEditor.root);
    return this;
  }

  /**
   * Moves cursor to end.
   */
  cursorToEnd() {
    const range = document.createRange(); //Create a range (a range is a like the selection but invisible)
    range.selectNodeContents(this.#textEditor.element);
    range.collapse(false);
    this.#selection.removeAllRanges();
    this.#selection.addRange(range);
    return this;
  }

  /**
   * Collapses a selection.
   *
   * @param {Node} node
   * @param {number} offset
   */
  collapse(node, offset) {
    const nodeOffset = (node.nodeType === Node.TEXT_NODE && offset >= node.nodeValue.length)
      ? node.nodeValue.length
      : offset

    return this.setSelection(
      node,
      nodeOffset,
      node,
      nodeOffset
    );
  }

  /**
   * Sets base and extent.
   *
   * @param {Node} anchorNode
   * @param {number} anchorOffset
   * @param {Node} [focusNode=anchorNode]
   * @param {number} [focusOffset=anchorOffset]
   */
  setSelection(anchorNode, anchorOffset, focusNode = anchorNode, focusOffset = anchorOffset) {
    if (!anchorNode.isConnected) {
      throw new Error('Invalid anchorNode')
    }
    if (!focusNode.isConnected) {
      throw new Error('Invalid focusNode')
    }
    if (this.#savedSelection) {
      this.#savedSelection.isCollapsed =
        focusNode === anchorNode && anchorOffset === focusOffset;
      this.#savedSelection.focusNode = focusNode;
      this.#savedSelection.focusOffset = focusOffset;
      this.#savedSelection.anchorNode = anchorNode;
      this.#savedSelection.anchorOffset = anchorOffset;

      this.#savedSelection.range.collapsed = this.#savedSelection.isCollapsed;
      const position = focusNode.compareDocumentPosition(anchorNode);
      if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
        this.#savedSelection.range.startContainer = focusNode;
        this.#savedSelection.range.startOffset = focusOffset;
        this.#savedSelection.range.endContainer = anchorNode;
        this.#savedSelection.range.endOffset = anchorOffset;
      } else {
        this.#savedSelection.range.startContainer = anchorNode;
        this.#savedSelection.range.startOffset = anchorOffset;
        this.#savedSelection.range.endContainer = focusNode;
        this.#savedSelection.range.endOffset = focusOffset;
      }
    } else {
      this.#anchorNode = anchorNode;
      this.#anchorOffset = anchorOffset;
      if (anchorNode === focusNode) {
        this.#focusNode = this.#anchorNode;
        this.#focusOffset = this.#anchorOffset;
        this.#selection.setPosition(anchorNode, anchorOffset);
      } else {
        this.#focusNode = focusNode;
        this.#focusOffset = focusOffset;
        this.#selection.setBaseAndExtent(
          anchorNode,
          anchorOffset,
          focusNode,
          focusOffset
        );
      }
    }
  }

  /**
   * Disposes the current resources.
   */
  dispose() {
    document.removeEventListener("selectionchange", this.#onSelectionChange);
    this.#textEditor = null;
    this.#ranges.clear();
    this.#ranges = null;
    this.#range = null;
    this.#selection = null;
    this.#focusNode = null;
    this.#anchorNode = null;
    this.#mutations.dispose();
    this.#mutations = null;
  }

  /**
   * Returns the current selection.
   *
   * @type {Selection}
   */
  get selection() {
    return this.#selection;
  }

  /**
   * Returns the current range.
   *
   * @type {Range}
   */
  get range() {
    return this.#range;
  }

  /**
   * Indicates the direction of the selection
   *
   * @type {SelectionDirection}
   */
  get direction() {
    if (this.isCollapsed) {
      return SelectionDirection.NONE;
    }
    if (this.focusNode !== this.anchorNode) {
      return this.startContainer === this.focusNode
        ? SelectionDirection.BACKWARD
        : SelectionDirection.FORWARD;
    }
    return this.focusOffset < this.anchorOffset
      ? SelectionDirection.BACKWARD
      : SelectionDirection.FORWARD;
  }

  /**
   * Indicates that the editor element has the
   * focus.
   *
   * @type {boolean}
   */
  get hasFocus() {
    return document.activeElement === this.#textEditor.element;
  }

  /**
   * Returns true if the selection is collapsed (caret)
   * or false otherwise.
   *
   * @type {boolean}
   */
  get isCollapsed() {
    if (this.#savedSelection) {
      return this.#savedSelection.isCollapsed;
    }
    return this.#selection.isCollapsed;
  }

  /**
   * Current or saved anchor node.
   *
   * @type {Node}
   */
  get anchorNode() {
    if (this.#savedSelection) {
      return this.#savedSelection.anchorNode;
    }
    return this.#anchorNode;
  }

  /**
   * Current or saved anchor offset.
   *
   * @type {number}
   */
  get anchorOffset() {
    if (this.#savedSelection) {
      return this.#savedSelection.anchorOffset;
    }
    return this.#selection.anchorOffset;
  }

  /**
   * Indicates that the caret is at the start of the node.
   *
   * @type {boolean}
   */
  get anchorAtStart() {
    return this.anchorOffset === 0;
  }

  /**
   * Indicates that the caret is at the end of the node.
   *
   * @type {boolean}
   */
  get anchorAtEnd() {
    return this.anchorOffset === this.anchorNode.nodeValue.length;
  }

  /**
   * Current or saved focus node.
   *
   * @type {Node}
   */
  get focusNode() {
    if (this.#savedSelection) {
      return this.#savedSelection.focusNode;
    }
    if (!this.#focusNode)
      console.trace("focusNode", this.#focusNode);
    return this.#focusNode;
  }

  /**
   * Current or saved focus offset.
   *
   * @type {number}
   */
  get focusOffset() {
    if (this.#savedSelection) {
      return this.#savedSelection.focusOffset;
    }
    return this.#focusOffset;
  }

  /**
   * Indicates that the caret is at the start of the node.
   *
   * @type {boolean}
   */
  get focusAtStart() {
    return this.focusOffset === 0;
  }

  /**
   * Indicates that the caret is at the end of the node.
   *
   * @type {boolean}
   */
  get focusAtEnd() {
    return this.focusOffset === this.focusNode.nodeValue.length;
  }

  /**
   * Returns the paragraph in the focus node
   * of the current selection.
   *
   * @type {HTMLElement|null}
   */
  get focusParagraph() {
    return getParagraph(this.focusNode);
  }

  /**
   * Returns the inline in the focus node
   * of the current selection.
   *
   * @type {HTMLElement|null}
   */
  get focusInline() {
    return getInline(this.focusNode);
  }

  /**
   * Returns the current paragraph in the anchor
   * node of the current selection.
   *
   * @type {HTMLElement|null}
   */
  get anchorParagraph() {
    return getParagraph(this.anchorNode);
  }

  /**
   * Returns the current inline in the anchor
   * node of the current selection.
   *
   * @type {HTMLElement|null}
   */
  get anchorInline() {
    return getInline(this.anchorNode);
  }

  /**
   * Start container of the current range.
   */
  get startContainer() {
    if (this.#savedSelection) {
      return this.#savedSelection?.range?.startContainer;
    }
    return this.#range?.startContainer;
  }

  /**
   * `startOffset` of the current range.
   *
   * @type {number|null}
   */
  get startOffset() {
    if (this.#savedSelection) {
      return this.#savedSelection?.range?.startOffset;
    }
    return this.#range?.startOffset;
  }

  /**
   * Start paragraph of the current range.
   *
   * @type {HTMLElement|null}
   */
  get startParagraph() {
    const startContainer = this.startContainer;
    if (!startContainer) return null;
    return getParagraph(startContainer);
  }

  /**
   * Start inline of the current page.
   *
   * @type {HTMLElement|null}
   */
  get startInline() {
    const startContainer = this.startContainer;
    if (!startContainer) return null;
    return getInline(startContainer);
  }

  /**
   * End container of the current range.
   *
   * @type {Node}
   */
  get endContainer() {
    if (this.#savedSelection) {
      return this.#savedSelection?.range?.endContainer;
    }
    return this.#range?.endContainer;
  }

  /**
   * `endOffset` of the current range
   *
   * @type {HTMLElement|null}
   */
  get endOffset() {
    if (this.#savedSelection) {
      return this.#savedSelection?.range?.endOffset;
    }
    return this.#range?.endOffset;
  }

  /**
   * Paragraph element of the `endContainer` of
   * the current range.
   *
   * @type {HTMLElement|null}
   */
  get endParagraph() {
    const endContainer = this.endContainer;
    if (!endContainer) return null;
    return getParagraph(endContainer);
  }

  /**
   * Inline element of the `endContainer` of
   * the current range.
   *
   * @type {HTMLElement|null}
   */
  get endInline() {
    const endContainer = this.endContainer;
    if (!endContainer) return null;
    return getInline(endContainer);
  }

  /**
   * Returns true if the anchor node and the focus
   * node are the same text nodes.
   *
   * @type {boolean}
   */
  get isTextSame() {
    return (
      this.isTextFocus === this.isTextAnchor &&
      this.focusNode === this.anchorNode
    );
  }

  /**
   * Indicates that focus node is a text node.
   *
   * @type {boolean}
   */
  get isTextFocus() {
    return this.focusNode.nodeType === Node.TEXT_NODE;
  }

  /**
   * Indicates that anchor node is a text node.
   *
   * @type {boolean}
   */
  get isTextAnchor() {
    return this.anchorNode.nodeType === Node.TEXT_NODE;
  }

  /**
   * Is true if the current focus node is a inline.
   *
   * @type {boolean}
   */
  get isInlineFocus() {
    return isInline(this.focusNode);
  }

  /**
   * Is true if the current anchor node is a inline.
   *
   * @type {boolean}
   */
  get isInlineAnchor() {
    return isInline(this.anchorNode);
  }

  /**
   * Is true if the current focus node is a paragraph.
   *
   * @type {boolean}
   */
  get isParagraphFocus() {
    return isParagraph(this.focusNode);
  }

  /**
   * Is true if the current anchor node is a paragraph.
   *
   * @type {boolean}
   */
  get isParagraphAnchor() {
    return isParagraph(this.anchorNode);
  }

  /**
   * Is true if the current focus node is a line break.
   *
   * @type {boolean}
   */
  get isLineBreakFocus() {
    return (
      isLineBreak(this.focusNode) ||
      (isInline(this.focusNode) && isLineBreak(this.focusNode.firstChild))
    );
  }

  /**
   * Indicates that we have multiple nodes selected.
   *
   * @type {boolean}
   */
  get isMulti() {
    return this.focusNode !== this.anchorNode;
  }

  /**
   * Indicates that we have selected multiple
   * paragraph elements.
   *
   * @type {boolean}
   */
  get isMultiParagraph() {
    return this.isMulti && this.focusParagraph !== this.anchorParagraph;
  }

  /**
   * Indicates that we have selected multiple
   * inline elements.
   *
   * @type {boolean}
   */
  get isMultiInline() {
    return this.isMulti && this.focusInline !== this.anchorInline;
  }

  /**
   * Indicates that the caret (only the caret)
   * is at the start of an inline.
   *
   * @type {boolean}
   */
  get isInlineStart() {
    if (!this.isCollapsed) return false;
    return isInlineStart(this.focusNode, this.focusOffset);
  }

  /**
   * Indicates that the caret (only the caret)
   * is at the end of an inline. This value doesn't
   * matter when dealing with selections.
   *
   * @type {boolean}
   */
  get isInlineEnd() {
    if (!this.isCollapsed) return false;
    return isInlineEnd(this.focusNode, this.focusOffset);
  }

  /**
   * Indicates that we're in the starting position of a paragraph.
   *
   * @type {boolean}
   */
  get isParagraphStart() {
    if (!this.isCollapsed) return false;
    return isParagraphStart(this.focusNode, this.focusOffset);
  }

  /**
   * Indicates that we're in the ending position of a paragraph.
   *
   * @type {boolean}
   */
  get isParagraphEnd() {
    if (!this.isCollapsed) return false;
    return isParagraphEnd(this.focusNode, this.focusOffset);
  }

  /**
   * Insert pasted fragment.
   *
   * @param {DocumentFragment} fragment
   */
  insertPaste(fragment) {
    const numParagraphs = fragment.children.length;
    if (this.isParagraphStart) {
      this.focusParagraph.before(fragment);
    } else if (this.isParagraphEnd) {
      this.focusParagraph.after(fragment);
    } else {
      const newParagraph = splitParagraph(
        this.focusParagraph,
        this.focusInline,
        this.focusOffset
      );
      this.focusParagraph.after(fragment, newParagraph);
    }
  }

  /**
   * Replaces data with pasted fragment
   *
   * @param {DocumentFragment} fragment
   */
  replaceWithPaste(fragment) {
    const numParagraphs = fragment.children.length;
    this.removeSelected();
    this.insertPaste(fragment);
  }

  /**
   * Replaces the current line break with text
   *
   * @param {string} text
   */
  replaceLineBreak(text) {
    const newText = new Text(text);
    this.focusInline.replaceChildren(newText);
    this.collapse(newText, text.length);
  }

  /**
   * Removes text forward from the current position.
   */
  removeForwardText() {
    this.#textNodeIterator.currentNode = this.focusNode;

    const removedData = removeForward(
      this.focusNode.nodeValue,
      this.focusOffset
    );

    if (this.focusNode.nodeValue !== removedData) {
      this.focusNode.nodeValue = removedData;
    }

    const paragraph = this.focusParagraph;
    if (!paragraph) throw new Error("Cannot find paragraph");
    const inline = this.focusInline;
    if (!inline) throw new Error("Cannot find inline");

    const nextTextNode = this.#textNodeIterator.nextNode();
    if (this.focusNode.nodeValue === "") {
      this.focusNode.remove();
    }

    if (paragraph.childNodes.length === 1 && inline.childNodes.length === 0) {
      const lineBreak = createLineBreak();
      inline.appendChild(lineBreak);
      return this.collapse(lineBreak, 0);
    } else if (
      paragraph.childNodes.length > 1 &&
      inline.childNodes.length === 0
    ) {
      inline.remove();
      return this.collapse(nextTextNode, 0);
    }
    return this.collapse(this.focusNode, this.focusOffset);
  }

  /**
   * Removes text backward from the current caret position.
   */
  removeBackwardText() {
    this.#textNodeIterator.currentNode = this.focusNode;

    // Remove the character from the string.
    const removedData = removeBackward(
      this.focusNode.nodeValue,
      this.focusOffset
    );

    if (this.focusNode.nodeValue !== removedData) {
      this.focusNode.nodeValue = removedData;
    }

    // If the focusNode has content we don't need to do
    // anything else.
    if (this.focusOffset - 1 > 0) {
      return this.collapse(this.focusNode, this.focusOffset - 1);
    }

    const paragraph = this.focusParagraph;
    if (!paragraph) throw new Error("Cannot find paragraph");
    const inline = this.focusInline;
    if (!inline) throw new Error("Cannot find inline");

    const previousTextNode = this.#textNodeIterator.previousNode();
    if (this.focusNode.nodeValue === "") {
      this.focusNode.remove();
    }

    if (paragraph.children.length === 1 && inline.childNodes.length === 0) {
      const lineBreak = createLineBreak();
      inline.appendChild(lineBreak);
      return this.collapse(lineBreak, 0);
    } else if (
      paragraph.children.length > 1 &&
      inline.childNodes.length === 0
    ) {
      inline.remove();
      return this.collapse(previousTextNode, getTextNodeLength(previousTextNode));
    }

    return this.collapse(this.focusNode, this.focusOffset - 1);
  }

  /**
   * Inserts some text in the caret position.
   *
   * @param {string} newText
   */
  insertText(newText) {
    this.focusNode.nodeValue = insertInto(
      this.focusNode.nodeValue,
      this.focusOffset,
      newText
    );
    this.#mutations.update(this.focusInline);
    return this.collapse(this.focusNode, this.focusOffset + newText.length);
  }

  /**
   * Replaces the currently focus element
   * with some text.
   *
   * @param {string} newText
   */
  insertIntoFocus(newText) {
    if (this.isTextFocus) {
      this.focusNode.nodeValue = insertInto(
        this.focusNode.nodeValue,
        this.focusOffset,
        newText
      );
    } else if (this.isLineBreakFocus) {
      const textNode = new Text(newText);
      this.focusNode.replaceWith(textNode);
      this.collapse(textNode, newText.length);
    } else {
      throw new Error('Unknown node type');
    }
  }

  /**
   * Replaces currently selected text.
   *
   * @param {string} newText
   */
  replaceText(newText) {
    const startOffset = Math.min(this.anchorOffset, this.focusOffset);
    const endOffset = Math.max(this.anchorOffset, this.focusOffset);
    if (this.isTextFocus) {
      this.focusNode.nodeValue = replaceWith(
        this.focusNode.nodeValue,
        startOffset,
        endOffset,
        newText
      );
    } else if (this.isLineBreakFocus) {
      this.focusNode.replaceWith(new Text(newText));
    } else {
      throw new Error('Unknown node type');
    }
    this.#mutations.update(this.focusInline);
    return this.collapse(this.focusNode, startOffset + newText.length);
  }

  /**
   * Replaces the selected inlines with new text.
   *
   * @param {string} newText
   */
  replaceInlines(newText) {
    const currentParagraph = this.focusParagraph;

    // This is the special (and fast) case where we're
    // removing everything inside a paragraph.
    if (
      this.startInline === currentParagraph.firstChild &&
      this.startOffset === 0 &&
      this.endInline === currentParagraph.lastChild &&
      this.endOffset === currentParagraph.lastChild.textContent.length
    ) {
      const newTextNode = new Text(newText);
      currentParagraph.replaceChildren(
        createInline(newTextNode, this.anchorInline.style)
      );
      return this.collapse(newTextNode, newTextNode.nodeValue.length);
    }

    this.removeSelected();
    this.insertIntoFocus(newText);

    /*
    this.focusNode.nodeValue = insertInto(
      this.focusNode.nodeValue,
      this.focusOffset,
      newText
    );
    */

    // FIXME: I'm not sure if we should merge inlines when they share the same styles.
    // For example: if we have > 2 inlines and the start inline and the end inline
    // share the same styles, maybe we should merge them?
    // mergeInlines(startInline, endInline);
    return this.collapse(this.focusNode, this.focusOffset + newText.length);
  }

  /**
   * Replaces paragraphs with text.
   *
   * @param {string} newText
   */
  replaceParagraphs(newText) {
    const currentParagraph = this.focusParagraph;

    this.removeSelected();
    this.insertIntoFocus(newText);

    for (const child of currentParagraph.children) {
      if (child.textContent === "") {
        child.remove();
      }
    }

    /*
    this.focusNode.nodeValue = insertInto(
      this.focusNode.nodeValue,
      this.focusOffset,
      newText
    );
    */
  }

  /**
   * Inserts a new paragraph after the current paragraph.
   */
  insertParagraphAfter() {
    const currentParagraph = this.focusParagraph;
    const newParagraph = createEmptyParagraph(this.#currentStyle);
    currentParagraph.after(newParagraph);
    this.#mutations.update(currentParagraph);
    this.#mutations.add(newParagraph);
    return this.collapse(newParagraph.firstChild.firstChild, 0);
  }

  /**
   * Inserts a new paragraph before the current paragraph.
   */
  insertParagraphBefore() {
    const currentParagraph = this.focusParagraph;
    const newParagraph = createEmptyParagraph(this.#currentStyle);
    currentParagraph.before(newParagraph);
    this.#mutations.update(currentParagraph);
    this.#mutations.add(newParagraph);
    return this.collapse(currentParagraph.firstChild.firstChild, 0);
  }

  /**
   * Splits the current paragraph.
   */
  splitParagraph() {
    const currentParagraph = this.focusParagraph;
    const newParagraph = splitParagraph(
      this.focusParagraph,
      this.focusInline,
      this.#focusOffset
    );
    this.focusParagraph.after(newParagraph);
    this.#mutations.update(currentParagraph);
    this.#mutations.add(newParagraph);
    return this.collapse(newParagraph.firstChild.firstChild, 0);
  }

  /**
   * Inserts a new paragraph.
   */
  insertParagraph() {
    if (this.isParagraphEnd) {
      return this.insertParagraphAfter();
    } else if (this.isParagraphStart) {
      return this.insertParagraphBefore();
    }
    return this.splitParagraph();
  }

  /**
   * Replaces the currently selected content with
   * a paragraph.
   */
  replaceWithParagraph() {
    const currentParagraph = this.focusParagraph;
    const currentInline = this.focusInline;

    this.removeSelected();

    const newParagraph = splitParagraph(
      currentParagraph,
      currentInline,
      this.focusOffset
    );
    currentParagraph.after(newParagraph);

    this.#mutations.update(currentParagraph);
    this.#mutations.add(newParagraph);

    // FIXME: Missing collapse?
  }

  /**
   * Removes a paragraph in backward direction.
   */
  removeBackwardParagraph() {
    const previousParagraph = this.focusParagraph.previousElementSibling;
    if (!previousParagraph) {
      return;
    }
    const paragraphToBeRemoved = this.focusParagraph;
    paragraphToBeRemoved.remove();
    const previousInline =
      previousParagraph.children.length > 1
        ? previousParagraph.lastElementChild
        : previousParagraph.firstChild;
    const previousOffset = isLineBreak(previousInline.firstChild)
      ? 0
      : previousInline.firstChild.nodeValue.length;
    this.#mutations.remove(paragraphToBeRemoved);
    return this.collapse(previousInline.firstChild, previousOffset);
  }

  /**
   * Merges the previous paragraph with the current paragraph.
   */
  mergeBackwardParagraph() {
    const currentParagraph = this.focusParagraph;
    const previousParagraph = this.focusParagraph.previousElementSibling;
    if (!previousParagraph) {
      return;
    }
    let previousInline = previousParagraph.lastChild;
    const previousOffset = getInlineLength(previousInline);
    if (isEmptyParagraph(previousParagraph)) {
      previousParagraph.replaceChildren(...currentParagraph.children);
      previousInline = previousParagraph.firstChild;
      currentParagraph.remove();
    } else {
      mergeParagraphs(previousParagraph, currentParagraph);
    }
    this.#mutations.remove(currentParagraph);
    this.#mutations.update(previousParagraph);
    return this.collapse(previousInline.firstChild, previousOffset);
  }

  /**
   * Merges the next paragraph with the current paragraph.
   */
  mergeForwardParagraph() {
    const currentParagraph = this.focusParagraph;
    const nextParagraph = this.focusParagraph.nextElementSibling;
    if (!nextParagraph) {
      return;
    }
    mergeParagraphs(this.focusParagraph, nextParagraph);
    this.#mutations.update(currentParagraph);
    this.#mutations.remove(nextParagraph);

    // FIXME: Missing collapse?
  }

  /**
   * Removes the forward paragraph.
   */
  removeForwardParagraph() {
    const nextParagraph = this.focusParagraph.nextSibling;
    if (!nextParagraph) {
      return;
    }
    const paragraphToBeRemoved = this.focusParagraph;
    paragraphToBeRemoved.remove();
    const nextInline = nextParagraph.firstChild;
    const nextOffset = this.focusOffset;
    this.#mutations.remove(paragraphToBeRemoved);
    return this.collapse(nextInline.firstChild, nextOffset);
  }

  /**
   * Cleans up all the affected paragraphs.
   *
   * @param {Set<HTMLDivElement>} affectedParagraphs
   * @param {Set<HTMLSpanElement>} affectedInlines
   */
  cleanUp(affectedParagraphs, affectedInlines) {
    // Remove empty inlines
    for (const inline of affectedInlines) {
      if (inline.textContent === "") {
        inline.remove();
        this.#mutations.remove(inline);
      }
    }

    // Remove empty paragraphs.
    for (const paragraph of affectedParagraphs) {
      if (paragraph.children.length === 0) {
        paragraph.remove();
        this.#mutations.remove(paragraph);
      }
    }
  }

  /**
   * Removes the selected content.
   *
   * @param {RemoveSelectedOptions} [options]
   */
  removeSelected(options) {
    if (this.isCollapsed) return;

    const affectedInlines = new Set();
    const affectedParagraphs = new Set();

    const startNode = getClosestTextNode(this.#range.startContainer);
    const endNode = getClosestTextNode(this.#range.endContainer);
    const startOffset = this.#range.startOffset;
    const endOffset = this.#range.endOffset;

    let previousNode = null;
    let nextNode = null;

    // This is the simplest case, when the startNode and the endNode
    // are the same and they're a textNode.
    if (startNode === endNode) {
      this.#textNodeIterator.currentNode = startNode;
      previousNode = this.#textNodeIterator.previousNode();

      this.#textNodeIterator.currentNode = startNode;
      nextNode = this.#textNodeIterator.nextNode();

      const inline = getInline(startNode);
      const paragraph = getParagraph(startNode);
      affectedInlines.add(inline);
      affectedParagraphs.add(paragraph);

      const newNodeValue = removeSlice(
        startNode.nodeValue,
        startOffset,
        endOffset
      );
      if (newNodeValue === "") {
        const lineBreak = createLineBreak();
        inline.replaceChildren(lineBreak);
        return this.collapse(lineBreak, 0);
      }
      startNode.nodeValue = newNodeValue;
      return this.collapse(startNode, startOffset);
    }

    // If startNode and endNode are different,
    // then we should process every text node from
    // start to end.

    // Select initial node.
    this.#textNodeIterator.currentNode = startNode;

    const startInline = getInline(startNode);
    const startParagraph = getParagraph(startNode);
    const endInline = getInline(endNode);
    const endParagraph = getParagraph(endNode);

    SafeGuard.start();
    do {
      SafeGuard.update();

      const currentNode = this.#textNodeIterator.currentNode;

      // We retrieve the inline and paragraph of the
      // current node.
      const inline = getInline(this.#textNodeIterator.currentNode);
      const paragraph = getParagraph(this.#textNodeIterator.currentNode);

      let shouldRemoveNodeCompletely = false;
      if (this.#textNodeIterator.currentNode === startNode) {
        if (startOffset === 0) {
          // We should remove this node completely.
          shouldRemoveNodeCompletely = true;
        } else {
          // We should remove this node partially.
          currentNode.nodeValue = currentNode.nodeValue.slice(0, startOffset);
        }
      } else if (this.#textNodeIterator.currentNode === endNode) {
        if (isLineBreak(endNode)
         || (isTextNode(endNode)
          && endOffset === endNode.nodeValue.length)) {
          // We should remove this node completely.
          shouldRemoveNodeCompletely = true;
        } else {
          // We should remove this node partially.
          currentNode.nodeValue = currentNode.nodeValue.slice(endOffset);
        }
      } else {
        // We should remove this node completely.
        shouldRemoveNodeCompletely = true;
      }

      this.#textNodeIterator.nextNode();

      // Realizamos el borrado del nodo actual.
      if (shouldRemoveNodeCompletely) {
        currentNode.remove();
        if (currentNode === startNode) {
          continue;
        }
        if (currentNode === endNode) {
          break;
        }

        if (inline.childNodes.length === 0) {
          inline.remove();
        }
        if (paragraph !== startParagraph && paragraph.children.length === 0) {
          paragraph.remove();
        }
      }

      if (currentNode === endNode) {
        break;
      }

    } while (this.#textNodeIterator.currentNode);

    if (startParagraph !== endParagraph) {
      const mergedParagraph = mergeParagraphs(startParagraph, endParagraph);
      if (mergedParagraph.children.length === 0) {
        const newEmptyInline = createEmptyInline(this.#currentStyle);
        mergedParagraph.appendChild(newEmptyInline);
        return this.collapse(newEmptyInline.firstChild, 0);
      }
    }

    if (startInline.childNodes.length === 0 && endInline.childNodes.length > 0) {
      startInline.remove();
      return this.collapse(endNode, 0);
    } else if (startInline.childNodes.length > 0 && endInline.childNodes.length === 0) {
      endInline.remove();
      return this.collapse(startNode, startOffset);
    } else if (startInline.childNodes.length === 0 && endInline.childNodes.length === 0) {
      const previousInline = startInline.previousElementSibling;
      const nextInline = endInline.nextElementSibling;
      startInline.remove();
      endInline.remove();
      if (previousInline) {
        return this.collapse(previousInline.firstChild, previousInline.firstChild.nodeValue.length);
      }
      if (nextInline) {
        return this.collapse(nextInline.firstChild, 0);
      }
      const newEmptyInline = createEmptyInline(this.#currentStyle);
      startParagraph.appendChild(newEmptyInline);
      return this.collapse(newEmptyInline.firstChild, 0);
    }

    return this.collapse(startNode, startOffset);
  }

  /**
   * Applies styles from the startNode to the endNode.
   *
   * @param {Node} startNode
   * @param {number} startOffset
   * @param {Node} endNode
   * @param {number} endOffset
   * @param {Object.<string,*>|CSSStyleDeclaration} newStyles
   * @returns {void}
   */
  #applyStylesTo(startNode, startOffset, endNode, endOffset, newStyles) {
    // Applies the necessary styles to the root element.
    const root = this.#textEditor.root;
    setRootStyles(root, newStyles);

    // If the startContainer and endContainer are the same
    // node, then we can apply styles directly to that
    // node.
    if (startNode === endNode && startNode.nodeType === Node.TEXT_NODE) {
      // The styles are applied to the node completelly.
      if (startOffset === 0 && endOffset === endNode.nodeValue.length) {
        const paragraph = this.startParagraph;
        const inline = this.startInline;
        setParagraphStyles(paragraph, newStyles);
        setInlineStyles(inline, newStyles);

        // The styles are applied to a part of the node.
      } else if (startOffset !== endOffset) {
        const paragraph = this.startParagraph;
        setParagraphStyles(paragraph, newStyles);
        const inline = this.startInline;
        const midText = startNode.splitText(startOffset);
        const endText = midText.splitText(endOffset - startOffset);
        const midInline = createInlineFrom(inline, midText, newStyles);
        inline.after(midInline);
        if (endText.length > 0) {
          const endInline = createInline(endText, inline.style);
          midInline.after(endInline);
        }

        // FIXME: This can change focus <-> anchor order.
        this.setSelection(midText, 0, midText, midText.nodeValue.length);

        // The styles are applied to the paragraph.
      } else {
        const paragraph = this.startParagraph;
        setParagraphStyles(paragraph, newStyles);
      }
      return this.#notifyStyleChange();

      // If the startContainer and endContainer are different
      // then we need to iterate through those nodes to apply
      // the styles.
    } else if (startNode !== endNode) {
      SafeGuard.start();
      const expectedEndNode = getClosestTextNode(endNode);
      this.#textNodeIterator.currentNode = getClosestTextNode(startNode);
      do {
        SafeGuard.update();

        const paragraph = getParagraph(this.#textNodeIterator.currentNode);
        setParagraphStyles(paragraph, newStyles);
        const inline = getInline(this.#textNodeIterator.currentNode);
        // If we're at the start node and offset is greater than 0
        // then we should split the inline and apply styles to that
        // new inline.
        if (
          this.#textNodeIterator.currentNode === startNode &&
          startOffset > 0
        ) {
          const newInline = splitInline(inline, startOffset);
          setInlineStyles(newInline, newStyles);
          inline.after(newInline);
          // If we're at the start node and offset is equal to 0
          // or current node is different to start node and
          // different to end node or we're at the end node
          // and the offset is equalto the node length
        } else if (
          (this.#textNodeIterator.currentNode === startNode &&
            startOffset === 0) ||
          (this.#textNodeIterator.currentNode !== startNode &&
            this.#textNodeIterator.currentNode !== endNode) ||
          (this.#textNodeIterator.currentNode === endNode &&
            endOffset === endNode.nodeValue.length)
        ) {
          setInlineStyles(inline, newStyles);

          // If we're at end node
        } else if (
          this.#textNodeIterator.currentNode === endNode &&
          endOffset < endNode.nodeValue.length
        ) {
          const newInline = splitInline(inline, endOffset);
          setInlineStyles(inline, newStyles);
          inline.after(newInline);
        }

        // We've reached the final node so we can return safely.
        if (this.#textNodeIterator.currentNode === expectedEndNode) return;

        this.#textNodeIterator.nextNode();
      } while (this.#textNodeIterator.currentNode);
    }

    return this.#notifyStyleChange();
  }

  /**
   * Applies styles to selection
   *
   * @param {Object.<string, *>} newStyles
   * @returns {void}
   */
  applyStyles(newStyles) {
    return this.#applyStylesTo(
      this.startContainer,
      this.startOffset,
      this.endContainer,
      this.endOffset,
      newStyles
    );
  }

  /**
   * BROWSER FIXES
   */
  fixInsertCompositionText() {
    this.#fixInsertCompositionText = true;
  }
}

export default SelectionController;