/**
 * 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 html = require('@lexical/html');
var list = require('@lexical/list');
var selection = require('@lexical/selection');
var utils = require('@lexical/utils');
var lexical = require('lexical');

/**
 * 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 $getHtmlContent(editor) {
  const selection = lexical.$getSelection();

  if (selection == null) {
    throw new Error('Expected valid LexicalSelection');
  } // If we haven't selected anything


  if (lexical.$isRangeSelection(selection) && selection.isCollapsed() || selection.getNodes().length === 0) {
    return '';
  }

  return html.$generateHtmlFromNodes(editor, selection);
} // TODO 0.6.0 Return a blank string instead
// TODO 0.6.0 Rename to $getJSON

function $getLexicalContent(editor) {
  const selection = lexical.$getSelection();

  if (selection == null) {
    throw new Error('Expected valid LexicalSelection');
  } // If we haven't selected anything


  if (lexical.$isRangeSelection(selection) && selection.isCollapsed() || selection.getNodes().length === 0) {
    return null;
  }

  return JSON.stringify($generateJSONFromSelectedNodes(editor, selection));
}
function $insertDataTransferForPlainText(dataTransfer, selection) {
  const text = dataTransfer.getData('text/plain');

  if (text != null) {
    selection.insertRawText(text);
  }
}
function $insertDataTransferForRichText(dataTransfer, selection, editor) {
  const lexicalString = dataTransfer.getData('application/x-lexical-editor');

  if (lexicalString) {
    try {
      const payload = JSON.parse(lexicalString);

      if (payload.namespace === editor._config.namespace && Array.isArray(payload.nodes)) {
        const nodes = $generateNodesFromSerializedNodes(payload.nodes);
        return $insertGeneratedNodes(editor, nodes, selection);
      } // eslint-disable-next-line no-empty

    } catch {}
  }

  const htmlString = dataTransfer.getData('text/html');

  if (htmlString) {
    try {
      const parser = new DOMParser();
      const dom = parser.parseFromString(htmlString, 'text/html');
      const nodes = html.$generateNodesFromDOM(editor, dom);
      return $insertGeneratedNodes(editor, nodes, selection); // eslint-disable-next-line no-empty
    } catch {}
  } // Multi-line plain text in rich text mode pasted as separate paragraphs
  // instead of single paragraph with linebreaks.


  const text = dataTransfer.getData('text/plain');

  if (text != null) {
    if (lexical.$isRangeSelection(selection)) {
      const lines = text.split(/\r?\n/);
      const linesLength = lines.length;

      for (let i = 0; i < linesLength; i++) {
        selection.insertText(lines[i]);

        if (i < linesLength - 1) {
          selection.insertParagraph();
        }
      }
    } else {
      selection.insertRawText(text);
    }
  }
}
function $insertGeneratedNodes(editor, nodes, selection) {
  const isSelectionInsideOfGrid = lexical.DEPRECATED_$isGridSelection(selection) || utils.$findMatchingParent(selection.anchor.getNode(), n => lexical.DEPRECATED_$isGridCellNode(n)) !== null && utils.$findMatchingParent(selection.focus.getNode(), n => lexical.DEPRECATED_$isGridCellNode(n)) !== null;

  if (isSelectionInsideOfGrid && nodes.length === 1 && lexical.DEPRECATED_$isGridNode(nodes[0])) {
    $mergeGridNodesStrategy(nodes, selection, false, editor);
    return;
  }

  $basicInsertStrategy(nodes, selection);
  return;
}

function $basicInsertStrategy(nodes, selection) {
  // Wrap text and inline nodes in paragraph nodes so we have all blocks at the top-level
  const topLevelBlocks = [];
  let currentBlock = null;
  let list$1 = null;

  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i];
    /**
     * There's no good way to add this to importDOM or importJSON directly,
     * so this is here in order to safely correct faulty clipboard data
     * that we can't control and avoid crashing the app.
     * https://github.com/facebook/lexical/issues/2405
     */

    if (list.$isListItemNode(node)) {
      if (list$1 == null) {
        list$1 = list.$createListNode('bullet');
        topLevelBlocks.push(list$1);
      }

      list$1.append(node);
      continue;
    } else if (list$1 != null) {
      list$1 = null;
    }

    const isLineBreakNode = lexical.$isLineBreakNode(node);

    if (isLineBreakNode || lexical.$isDecoratorNode(node) && node.isInline() || lexical.$isElementNode(node) && node.isInline() || lexical.$isTextNode(node)) {
      if (currentBlock === null) {
        currentBlock = lexical.$createParagraphNode();
        topLevelBlocks.push(currentBlock); // In the case of LineBreakNode, we just need to
        // add an empty ParagraphNode to the topLevelBlocks.

        if (isLineBreakNode) {
          continue;
        }
      }

      if (currentBlock !== null) {
        currentBlock.append(node);
      }
    } else {
      topLevelBlocks.push(node);
      currentBlock = null;
    }
  }

  if (lexical.$isRangeSelection(selection)) {
    selection.insertNodes(topLevelBlocks);
  } else if (lexical.DEPRECATED_$isGridSelection(selection)) {
    // If there's an active grid selection and a non grid is pasted, add to the anchor.
    const anchorCell = selection.anchor.getNode();

    if (!lexical.DEPRECATED_$isGridCellNode(anchorCell)) {
      {
        throw Error(`Expected Grid Cell in Grid Selection`);
      }
    }

    anchorCell.append(...topLevelBlocks);
  }
}

function $mergeGridNodesStrategy(nodes, selection, isFromLexical, editor) {
  if (nodes.length !== 1 || !lexical.DEPRECATED_$isGridNode(nodes[0])) {
    {
      throw Error(`$mergeGridNodesStrategy: Expected Grid insertion.`);
    }
  }

  const newGrid = nodes[0];
  const newGridRows = newGrid.getChildren();
  const newColumnCount = newGrid.getFirstChildOrThrow().getChildrenSize();
  const newRowCount = newGrid.getChildrenSize();
  const gridCellNode = utils.$findMatchingParent(selection.anchor.getNode(), n => lexical.DEPRECATED_$isGridCellNode(n));
  const gridRowNode = gridCellNode && utils.$findMatchingParent(gridCellNode, n => lexical.DEPRECATED_$isGridRowNode(n));
  const gridNode = gridRowNode && utils.$findMatchingParent(gridRowNode, n => lexical.DEPRECATED_$isGridNode(n));

  if (!lexical.DEPRECATED_$isGridCellNode(gridCellNode) || !lexical.DEPRECATED_$isGridRowNode(gridRowNode) || !lexical.DEPRECATED_$isGridNode(gridNode)) {
    {
      throw Error(`$mergeGridNodesStrategy: Expected selection to be inside of a Grid.`);
    }
  }

  const startY = gridRowNode.getIndexWithinParent();
  const stopY = Math.min(gridNode.getChildrenSize() - 1, startY + newRowCount - 1);
  const startX = gridCellNode.getIndexWithinParent();
  const stopX = Math.min(gridRowNode.getChildrenSize() - 1, startX + newColumnCount - 1);
  const fromX = Math.min(startX, stopX);
  const fromY = Math.min(startY, stopY);
  const toX = Math.max(startX, stopX);
  const toY = Math.max(startY, stopY);
  const gridRowNodes = gridNode.getChildren();
  let newRowIdx = 0;
  let newAnchorCellKey;
  let newFocusCellKey;

  for (let r = fromY; r <= toY; r++) {
    const currentGridRowNode = gridRowNodes[r];

    if (!lexical.DEPRECATED_$isGridRowNode(currentGridRowNode)) {
      {
        throw Error(`getNodes: expected to find GridRowNode`);
      }
    }

    const newGridRowNode = newGridRows[newRowIdx];

    if (!lexical.DEPRECATED_$isGridRowNode(newGridRowNode)) {
      {
        throw Error(`getNodes: expected to find GridRowNode`);
      }
    }

    const gridCellNodes = currentGridRowNode.getChildren();
    const newGridCellNodes = newGridRowNode.getChildren();
    let newColumnIdx = 0;

    for (let c = fromX; c <= toX; c++) {
      const currentGridCellNode = gridCellNodes[c];

      if (!lexical.DEPRECATED_$isGridCellNode(currentGridCellNode)) {
        {
          throw Error(`getNodes: expected to find GridCellNode`);
        }
      }

      const newGridCellNode = newGridCellNodes[newColumnIdx];

      if (!lexical.DEPRECATED_$isGridCellNode(newGridCellNode)) {
        {
          throw Error(`getNodes: expected to find GridCellNode`);
        }
      }

      if (r === fromY && c === fromX) {
        newAnchorCellKey = currentGridCellNode.getKey();
      } else if (r === toY && c === toX) {
        newFocusCellKey = currentGridCellNode.getKey();
      }

      const originalChildren = currentGridCellNode.getChildren();
      newGridCellNode.getChildren().forEach(child => {
        if (lexical.$isTextNode(child)) {
          const paragraphNode = lexical.$createParagraphNode();
          paragraphNode.append(child);
          currentGridCellNode.append(child);
        } else {
          currentGridCellNode.append(child);
        }
      });
      originalChildren.forEach(n => n.remove());
      newColumnIdx++;
    }

    newRowIdx++;
  }

  if (newAnchorCellKey && newFocusCellKey) {
    const newGridSelection = lexical.DEPRECATED_$createGridSelection();
    newGridSelection.set(gridNode.getKey(), newAnchorCellKey, newFocusCellKey);
    lexical.$setSelection(newGridSelection);
    editor.dispatchCommand(lexical.SELECTION_CHANGE_COMMAND, undefined);
  }
}

function exportNodeToJSON(node) {
  const serializedNode = node.exportJSON();
  const nodeClass = node.constructor; // @ts-expect-error TODO Replace Class utility type with InstanceType

  if (serializedNode.type !== nodeClass.getType()) {
    {
      throw Error(`LexicalNode: Node ${nodeClass.name} does not implement .exportJSON().`);
    }
  } // @ts-expect-error TODO Replace Class utility type with InstanceType


  const serializedChildren = serializedNode.children;

  if (lexical.$isElementNode(node)) {
    if (!Array.isArray(serializedChildren)) {
      {
        throw Error(`LexicalNode: Node ${nodeClass.name} is an element but .exportJSON() does not have a children array.`);
      }
    }
  }

  return serializedNode;
}

function $appendNodesToJSON(editor, selection$1, currentNode, targetArray = []) {
  let shouldInclude = selection$1 != null ? currentNode.isSelected() : true;
  const shouldExclude = lexical.$isElementNode(currentNode) && currentNode.excludeFromCopy('html');
  let target = currentNode;

  if (selection$1 !== null) {
    let clone = selection.$cloneWithProperties(currentNode);
    clone = lexical.$isTextNode(clone) && selection$1 != null ? selection.$sliceSelectedTextNodeContent(selection$1, clone) : clone;
    target = clone;
  }

  const children = lexical.$isElementNode(target) ? target.getChildren() : [];
  const serializedNode = exportNodeToJSON(target); // TODO: TextNode calls getTextContent() (NOT node.__text) within it's exportJSON method
  // which uses getLatest() to get the text from the original node with the same key.
  // This is a deeper issue with the word "clone" here, it's still a reference to the
  // same node as far as the LexicalEditor is concerned since it shares a key.
  // We need a way to create a clone of a Node in memory with it's own key, but
  // until then this hack will work for the selected text extract use case.

  if (lexical.$isTextNode(target)) {
    serializedNode.text = target.__text;
  }

  for (let i = 0; i < children.length; i++) {
    const childNode = children[i];
    const shouldIncludeChild = $appendNodesToJSON(editor, selection$1, childNode, serializedNode.children);

    if (!shouldInclude && lexical.$isElementNode(currentNode) && shouldIncludeChild && currentNode.extractWithChild(childNode, selection$1, 'clone')) {
      shouldInclude = true;
    }
  }

  if (shouldInclude && !shouldExclude) {
    targetArray.push(serializedNode);
  } else if (Array.isArray(serializedNode.children)) {
    for (let i = 0; i < serializedNode.children.length; i++) {
      const serializedChildNode = serializedNode.children[i];
      targetArray.push(serializedChildNode);
    }
  }

  return shouldInclude;
} // TODO why $ function with Editor instance?


function $generateJSONFromSelectedNodes(editor, selection) {
  const nodes = [];
  const root = lexical.$getRoot();
  const topLevelChildren = root.getChildren();

  for (let i = 0; i < topLevelChildren.length; i++) {
    const topLevelNode = topLevelChildren[i];
    $appendNodesToJSON(editor, selection, topLevelNode, nodes);
  }

  return {
    namespace: editor._config.namespace,
    nodes
  };
}
function $generateNodesFromSerializedNodes(serializedNodes) {
  const nodes = [];

  for (let i = 0; i < serializedNodes.length; i++) {
    const serializedNode = serializedNodes[i];
    const node = lexical.$parseSerializedNode(serializedNode);

    if (lexical.$isTextNode(node)) {
      selection.$addNodeStyle(node);
    }

    nodes.push(node);
  }

  return nodes;
}
const EVENT_LATENCY = 50;
let clipboardEventTimeout = null; // TODO custom selection
// TODO potentially have a node customizable version for plain text

async function copyToClipboard__EXPERIMENTAL(editor, event) {
  if (clipboardEventTimeout !== null) {
    // Prevent weird race conditions that can happen when this function is run multiple times
    // synchronously. In the future, we can do better, we can cancel/override the previously running job.
    return false;
  }

  if (event !== null) {
    return new Promise((resolve, reject) => {
      editor.update(() => {
        resolve($copyToClipboardEvent(editor, event));
      });
    });
  }

  const rootElement = editor.getRootElement();
  const domSelection = document.getSelection();

  if (rootElement === null || domSelection === null) {
    return false;
  }

  const element = document.createElement('span');
  element.style.cssText = 'position: fixed; top: -1000px;';
  element.append(document.createTextNode('#'));
  rootElement.append(element);
  const range = new Range();
  range.setStart(element, 0);
  range.setEnd(element, 1);
  domSelection.removeAllRanges();
  domSelection.addRange(range);
  return new Promise((resolve, reject) => {
    const removeListener = editor.registerCommand(lexical.COPY_COMMAND, secondEvent => {
      if (secondEvent instanceof ClipboardEvent) {
        removeListener();

        if (clipboardEventTimeout !== null) {
          window.clearTimeout(clipboardEventTimeout);
          clipboardEventTimeout = null;
        }

        resolve($copyToClipboardEvent(editor, secondEvent));
      } // Block the entire copy flow while we wait for the next ClipboardEvent


      return true;
    }, lexical.COMMAND_PRIORITY_CRITICAL); // If the above hack execCommand hack works, this timeout code should never fire. Otherwise,
    // the listener will be quickly freed so that the user can reuse it again

    clipboardEventTimeout = window.setTimeout(() => {
      removeListener();
      clipboardEventTimeout = null;
      resolve(false);
    }, EVENT_LATENCY);
    document.execCommand('copy');
    element.remove();
  });
} // TODO shouldn't pass editor (pass namespace directly)

function $copyToClipboardEvent(editor, event) {
  event.preventDefault();
  const clipboardData = event.clipboardData;

  if (clipboardData === null) {
    return false;
  }

  const selection = lexical.$getSelection();
  const htmlString = $getHtmlContent(editor);
  const lexicalString = $getLexicalContent(editor);
  let plainString = '';

  if (selection !== null) {
    plainString = selection.getTextContent();
  }

  if (htmlString !== null) {
    clipboardData.setData('text/html', htmlString);
  }

  if (lexicalString !== null) {
    clipboardData.setData('application/x-lexical-editor', lexicalString);
  }

  clipboardData.setData('text/plain', plainString);
  return true;
}

exports.$generateJSONFromSelectedNodes = $generateJSONFromSelectedNodes;
exports.$generateNodesFromSerializedNodes = $generateNodesFromSerializedNodes;
exports.$getHtmlContent = $getHtmlContent;
exports.$getLexicalContent = $getLexicalContent;
exports.$insertDataTransferForPlainText = $insertDataTransferForPlainText;
exports.$insertDataTransferForRichText = $insertDataTransferForRichText;
exports.$insertGeneratedNodes = $insertGeneratedNodes;
exports.copyToClipboard__EXPERIMENTAL = copyToClipboard__EXPERIMENTAL;
