/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 */

'use strict';

var lexical = require('lexical');
var utils = require('@lexical/utils');

/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 */

const NO_IDS = [];

/** @noInheritDoc */
class MarkNode extends lexical.ElementNode {
  /** @internal */
  __ids;
  static getType() {
    return 'mark';
  }
  static clone(node) {
    return new MarkNode(node.__ids, node.__key);
  }
  afterCloneFrom(prevNode) {
    super.afterCloneFrom(prevNode);
    this.__ids = prevNode.__ids;
  }
  static importDOM() {
    return null;
  }
  static importJSON(serializedNode) {
    return $createMarkNode().updateFromJSON(serializedNode);
  }
  updateFromJSON(serializedNode) {
    return super.updateFromJSON(serializedNode).setIDs(serializedNode.ids);
  }
  exportJSON() {
    return {
      ...super.exportJSON(),
      ids: this.getIDs()
    };
  }
  constructor(ids = NO_IDS, key) {
    super(key);
    this.__ids = ids;
  }
  createDOM(config) {
    const element = document.createElement('mark');
    utils.addClassNamesToElement(element, config.theme.mark);
    if (this.__ids.length > 1) {
      utils.addClassNamesToElement(element, config.theme.markOverlap);
    }
    return element;
  }
  updateDOM(prevNode, element, config) {
    const prevIDs = prevNode.__ids;
    const nextIDs = this.__ids;
    const prevIDsCount = prevIDs.length;
    const nextIDsCount = nextIDs.length;
    const overlapTheme = config.theme.markOverlap;
    if (prevIDsCount !== nextIDsCount) {
      if (prevIDsCount === 1) {
        if (nextIDsCount === 2) {
          utils.addClassNamesToElement(element, overlapTheme);
        }
      } else if (nextIDsCount === 1) {
        utils.removeClassNamesFromElement(element, overlapTheme);
      }
    }
    return false;
  }
  hasID(id) {
    return this.getIDs().includes(id);
  }
  getIDs() {
    return Array.from(this.getLatest().__ids);
  }
  setIDs(ids) {
    const self = this.getWritable();
    self.__ids = ids;
    return self;
  }
  addID(id) {
    const self = this.getWritable();
    return self.__ids.includes(id) ? self : self.setIDs([...self.__ids, id]);
  }
  deleteID(id) {
    const self = this.getWritable();
    const idx = self.__ids.indexOf(id);
    if (idx === -1) {
      return self;
    }
    const ids = Array.from(self.__ids);
    ids.splice(idx, 1);
    return self.setIDs(ids);
  }
  insertNewAfter(selection, restoreSelection = true) {
    const markNode = $createMarkNode(this.__ids);
    this.insertAfter(markNode, restoreSelection);
    return markNode;
  }
  canInsertTextBefore() {
    return false;
  }
  canInsertTextAfter() {
    return false;
  }
  canBeEmpty() {
    return false;
  }
  isInline() {
    return true;
  }
  extractWithChild(child, selection, destination) {
    if (!lexical.$isRangeSelection(selection) || destination === 'html') {
      return false;
    }
    const anchor = selection.anchor;
    const focus = selection.focus;
    const anchorNode = anchor.getNode();
    const focusNode = focus.getNode();
    const isBackward = selection.isBackward();
    const selectionLength = isBackward ? anchor.offset - focus.offset : focus.offset - anchor.offset;
    return this.isParentOf(anchorNode) && this.isParentOf(focusNode) && this.getTextContent().length === selectionLength;
  }
  excludeFromCopy(destination) {
    return destination !== 'clone';
  }
}
function $createMarkNode(ids = NO_IDS) {
  return lexical.$applyNodeReplacement(new MarkNode(ids));
}
function $isMarkNode(node) {
  return node instanceof MarkNode;
}

/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 */

function $unwrapMarkNode(node) {
  const children = node.getChildren();
  let target = null;
  for (let i = 0; i < children.length; i++) {
    const child = children[i];
    if (target === null) {
      node.insertBefore(child);
    } else {
      target.insertAfter(child);
    }
    target = child;
  }
  node.remove();
}
function $wrapSelectionInMarkNode(selection, isBackward, id, createNode) {
  // Force a forwards selection since append is used, ignore the argument.
  // A new selection is used to avoid side-effects of flipping the given
  // selection
  const forwardSelection = lexical.$createRangeSelection();
  const [startPoint, endPoint] = selection.isBackward() ? [selection.focus, selection.anchor] : [selection.anchor, selection.focus];
  forwardSelection.anchor.set(startPoint.key, startPoint.offset, startPoint.type);
  forwardSelection.focus.set(endPoint.key, endPoint.offset, endPoint.type);
  let currentNodeParent;
  let lastCreatedMarkNode;

  // Note that extract will split text nodes at the boundaries
  const nodes = forwardSelection.extract();
  // We only want wrap adjacent text nodes, line break nodes
  // and inline element nodes. For decorator nodes and block
  // element nodes, we step out of their boundary and start
  // again after, if there are more nodes.
  for (const node of nodes) {
    if (lexical.$isElementNode(lastCreatedMarkNode) && lastCreatedMarkNode.isParentOf(node)) {
      // If the current node is a child of the last created mark node, there is nothing to do here
      continue;
    }
    let targetNode = null;
    if (lexical.$isTextNode(node)) {
      // Case 1: The node is a text node and we can include it
      targetNode = node;
    } else if ($isMarkNode(node)) {
      // Case 2: the node is a mark node and we can ignore it as a target,
      // moving on to its children. Note that when we make a mark inside
      // another mark, it may ultimately be unnested by a call to
      // `registerNestedElementResolver<MarkNode>` somewhere else in the
      // codebase.
      continue;
    } else if ((lexical.$isElementNode(node) || lexical.$isDecoratorNode(node)) && node.isInline()) {
      // Case 3: inline element/decorator nodes can be added in their entirety
      // to the new mark
      targetNode = node;
    }
    if (targetNode !== null) {
      // Now that we have a target node for wrapping with a mark, we can run
      // through special cases.
      if (targetNode && targetNode.is(currentNodeParent)) {
        // The current node is a child of the target node to be wrapped, there
        // is nothing to do here.
        continue;
      }
      const parentNode = targetNode.getParent();
      if (parentNode == null || !parentNode.is(currentNodeParent)) {
        // If the parent node is not the current node's parent node, we can
        // clear the last created mark node.
        lastCreatedMarkNode = undefined;
      }
      currentNodeParent = parentNode;
      if (lastCreatedMarkNode === undefined) {
        // If we don't have a created mark node, we can make one
        const createMarkNode = createNode || $createMarkNode;
        lastCreatedMarkNode = createMarkNode([id]);
        targetNode.insertBefore(lastCreatedMarkNode);
      }

      // Add the target node to be wrapped in the latest created mark node
      lastCreatedMarkNode.append(targetNode);
    } else {
      // If we don't have a target node to wrap we can clear our state and
      // continue on with the next node
      currentNodeParent = undefined;
      lastCreatedMarkNode = undefined;
    }
  }
  // Make selection collapsed at the end
  if (lexical.$isElementNode(lastCreatedMarkNode)) {
    if (isBackward) {
      lastCreatedMarkNode.selectStart();
    } else {
      lastCreatedMarkNode.selectEnd();
    }
  }
}
function $getMarkIDs(node, offset) {
  let currentNode = node;
  while (currentNode !== null) {
    if ($isMarkNode(currentNode)) {
      return currentNode.getIDs();
    } else if (lexical.$isTextNode(currentNode) && offset === currentNode.getTextContentSize()) {
      const nextSibling = currentNode.getNextSibling();
      if ($isMarkNode(nextSibling)) {
        return nextSibling.getIDs();
      }
    }
    currentNode = currentNode.getParent();
  }
  return null;
}

/**
 * Configures {@link MarkNode}
 */
const MarkExtension = lexical.defineExtension({
  name: '@lexical/mark',
  nodes: () => [MarkNode]
});

exports.$createMarkNode = $createMarkNode;
exports.$getMarkIDs = $getMarkIDs;
exports.$isMarkNode = $isMarkNode;
exports.$unwrapMarkNode = $unwrapMarkNode;
exports.$wrapSelectionInMarkNode = $wrapSelectionInMarkNode;
exports.MarkExtension = MarkExtension;
exports.MarkNode = MarkNode;
