/*
 * 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.
 */

import UIKit

/**
 Used to initialize an Editor with a Theme and some Plugins.

 Note that you shouldn't use an EditorConfig to initialize multiple Editors, because each instantiated ``Plugin`` maintains state
 about the editor it is attached to. In the future we will hopefully improve this API, possibly replacing the Plugins array
 with a closure to build new Plugin objects, which would let us remove this restriction.
 */
@objc public class EditorConfig: NSObject {
  let theme: Theme
  let plugins: [Plugin]

  @objc public init(theme: Theme, plugins: [Plugin]) {
    self.theme = theme
    self.plugins = plugins
  }
}

internal enum DecoratorCacheItem {
  case needsCreation
  case cachedView(UIView)
  case unmountedCachedView(UIView)
  case needsDecorating(UIView)

  var view: UIView? {
    switch self {
    case .needsCreation:
      return nil
    case .cachedView(let aView):
      return aView
    case .unmountedCachedView(let aView):
      return aView
    case .needsDecorating(let aView):
      return aView
    }
  }
}

/// Editor instances are the core thing that wires everything together.
///
/// The Editor is your entry point to everything Lexical can do. In order to make any changes to
/// an ``EditorState`` (aka Lexical's data model), you must go through the Editor's ``update(_:)`` method.
public class Editor: NSObject {
  internal static var maxUpdateCount = 99

  private var editorState: EditorState
  private var pendingEditorState: EditorState?
  private var theme: Theme

  internal var textStorage: TextStorage? {
    frontend?.textStorage
  }

  internal weak var frontend: Frontend? {
    didSet {
      if pendingEditorState != nil {
        if let textStorage {
          textStorage.mode = .controllerMode
          textStorage.replaceCharacters(in: NSRange(location: 0, length: textStorage.string.lengthAsNSString()), with: "")
          textStorage.mode = .none
        }
        try? update {}
      }
    }
  }

  internal var infiniteUpdateLoopCount = 0
  // keyCounter is the next available node key to be used.
  internal var keyCounter: Int = 0

  // Transforms are defined as functions that operate on nodes. In the JS code, functions are
  // equatable but Swift for a variety of reasons does not support this. To keep track of transforms
  // we assign them an ID as they are inserted.
  internal var transformCounter: Int = 0
  internal var isRecoveringFromError: Bool = false

  // See description in RangeCache.swift.
  internal var rangeCache: [NodeKey: RangeCacheItem] = [:]
  internal var dirtyNodes: DirtyNodeMap = [:]
  internal var cloneNotNeeded: Set<NodeKey> = Set()
  internal var normalizedNodes: Set<NodeKey> = Set()

  // Used for deserialization and registration of nodes. Lexical's built-in nodes are registered
  // by default.
  internal var registeredNodes: [NodeType: Node.Type] = [.root: RootNode.self, .text: TextNode.self, .element: ElementNode.self, .heading: HeadingNode.self, .paragraph: ParagraphNode.self, .quote: QuoteNode.self]

  internal var nodeTransforms: [NodeType: [(Int, NodeTransform)]] = [:]

  // Used to help co-ordinate selection and events
  internal var compositionKey: NodeKey?
  public var dirtyType: DirtyType = .noDirtyNodes // TODO: I made this public to work around an issue in playground. @amyworrall
  internal var featureFlags: FeatureFlags = FeatureFlags()

  // Used for storing editor listener events
  internal var listeners = Listeners()

  // Used for storing and dispatching command listeners
  internal var commands: Commands = [:]

  internal var plugins: [Plugin]

  // Used to cache decorators
  internal var decoratorCache: [NodeKey: DecoratorCacheItem] = [:]

  // Headless mode runs without the reconciler
  private var headless: Bool = false

  // Parent editors are used for nested editors inside decorator nodes
  public weak var parentEditor: Editor?

  // MARK: - Initialisation

  /// Initialises a new Editor.
  /// - Parameter editorConfig: Settable options for the editor, including a Theme and Plugins.
  ///
  /// You will usually not need to directly init an Editor; instead you will create a TextView
  /// which will initialise the Editor for you.
  public init(editorConfig: EditorConfig) {
    editorState = EditorState()
    guard let rootNodeKey = editorState.getRootNode()?.key else {
      fatalError("Expected root node key when creating new editor state")
    }
    rangeCache[rootNodeKey] = RangeCacheItem()
    theme = editorConfig.theme
    plugins = editorConfig.plugins
    super.init()
    initializePlugins(plugins)

    // registering custom drawing for built in nodes
    try? registerCustomDrawing(customAttribute: .inlineCodeBackgroundColor, layer: .background, granularity: .characterRuns, handler: TextNode.inlineCodeBackgroundDrawing)
    try? registerCustomDrawing(customAttribute: .codeBlockCustomDrawing, layer: .background, granularity: .contiguousParagraphs, handler: CodeNode.codeBlockBackgroundDrawing)
    try? registerCustomDrawing(customAttribute: .quoteCustomDrawing, layer: .background, granularity: .contiguousParagraphs, handler: QuoteNode.quoteBackgroundDrawing)

    resetEditor()
  }

  convenience init(featureFlags: FeatureFlags, editorConfig: EditorConfig) {
    self.init(editorConfig: editorConfig)
    self.featureFlags = featureFlags
  }

  /// This method is only used for testing purposes
  override convenience init() {
    self.init(editorConfig: EditorConfig(theme: Theme(), plugins: []))
  }

  /**
   Create a new editor in Headless mode.

   Headless mode is a version of Lexical that does not reconcile or produce output. It is
   useful for quickly manipulating a Lexical data model.
   */
  public static func createHeadless(editorConfig: EditorConfig) -> Editor {
    let editor = Editor(editorConfig: editorConfig)
    editor.headless = true
    return editor
  }

  private func initializePlugins(_ plugins: [Plugin]) {
    plugins.forEach { plugin in
      plugin.setUp(editor: self)
    }
  }

  deinit {
    plugins.forEach { plugin in
      plugin.tearDown()
    }
  }

  // MARK: - Accessing editor state

  /// Allows you to make changes to the EditorState.
  /// - Parameter closure: Code to run in order to modify the editor state.
  /// Functions that work with nodes or selections can only be used inside a read or update closure.
  ///
  /// The `update()` function is the primary way you make proactive changes to a Lexical ``EditorState``.
  ///
  /// Once your update closure has run, the Reconciler will be run. This will apply mutations to the
  /// text view to make it track the changes you have just made. (Note that if update calls are nested,
  /// the reconciler will run once the outer-most update closure returns.)
  ///
  /// > Important: Your code within the `update()` closure must run synchronously on the thread that
  /// Lexical calls it on. Do not dispatch to another thread!
  public func update(_ closure: () throws -> Void) throws {
    try beginUpdate(closure, mode: UpdateBehaviourModificationMode())
  }

  /// Convenience function to read the Editor's current EditorState.
  /// - Parameter closure: Inside this closure you can call functions that read (but not mutate) nodes.
  ///
  /// This function is syntactic sugar over calling ``getEditorState()`` then ``EditorState/read(closure:)``.
  /// Note if you want to return a value, using the function directly on the ``EditorState`` is better.
  public func read(_ closure: () throws -> Void) throws {
    try beginRead(closure)
  }

  /// Returns the current editor state.
  /// - Returns: The editor state.
  ///
  /// > Note: If there is a pending ``EditorState`` (i.e. you're in the middle of an update block and
  /// have mutated the EditorState), this function ignores the pending EditorState and returns the
  /// active (old) EditorState.
  @objc public func getEditorState() -> EditorState {
    editorState
  }

  //  /// Returns the TextView attached to this Editor.
  //  /// - Returns: the TextView
  //  func getTextView() -> TextView? {
  //    textView
  //  }

  // MARK: - Registration

  public func registerErrorListener(listener: @escaping ErrorListener) -> () -> Void {
    let uuid = UUID()

    self.listeners.errors[uuid] = listener

    return { [weak self] in
      guard let self else { return }
      self.listeners.update.removeValue(forKey: uuid)
    }
  }

  /// Registers a closure to be run whenever the ``EditorState`` changes.
  /// - Parameter listener: The code to run when the ``EditorState`` changes.
  /// - Returns: A closure to remove the update listener
  public func registerUpdateListener(listener: @escaping UpdateListener) -> () -> Void {
    let uuid = UUID()
    self.listeners.update[uuid] = listener
    return { [weak self] in
      guard let self else { return }
      self.listeners.update.removeValue(forKey: uuid)
    }
  }

  /// Registers a closure to be run whenever the reconciled text content changes.
  /// - Parameter listener: The code to run when the text content changes
  /// - Returns: A closure to remove the text content listener
  public func registerTextContentListener(listener: @escaping TextContentListener) -> () -> Void {
    let uuid = UUID()

    self.listeners.textContent[uuid] = listener

    return { [weak self] in
      guard let self else { return }
      self.listeners.textContent.removeValue(forKey: uuid)
    }
  }

  /// Registers a handler to be called whenever a certain command is dispatched.
  /// - Parameters:
  ///   - type: The command you want to listen for. (This can be a built in command or a custom one added by your plugin.)
  ///   - listener: The code to run when the command is dispatched.
  ///   - priority: The priority for your handler. Higher priority handlers run before lower priority handlers.
  /// - Returns: A closure to remove the command handler.
  public func registerCommand(type: CommandType, listener: @escaping CommandListener, priority: CommandPriority = CommandPriority.Editor) -> () -> Void {
    let uuid = UUID()

    if self.commands[type] == nil {
      self.commands.updateValue(
        [
          CommandPriority.Editor: [:],
          CommandPriority.Low: [:],
          CommandPriority.Normal: [:],
          CommandPriority.High: [:],
          CommandPriority.Critical: [:]
        ],
        forKey: type
      )
    }

    self.commands[type]?[priority]?[uuid] = listener

    return { [weak self] in
      guard let self else { return }

      self.commands[type]?[priority]?.removeValue(forKey: uuid)
    }
  }

  /// Registers a new Node subclass
  /// - Parameters:
  ///   - nodeType: The type (name) of your node
  ///   - constructor: A constructor to deserialise your node from JSON.
  ///
  ///   Node subclasses must be registered before use, in order that Lexical knows how to serialise them etc.
  public func registerNode(nodeType: NodeType, class klass: Node.Type) throws {
    if self.registeredNodes[nodeType] != nil {
      throw LexicalError.invariantViolation("Node type \(nodeType) already registered")
    }

    registeredNodes[nodeType] = klass
  }

  public func getRegisteredNodes() -> [NodeType: Node.Type] {
    return registeredNodes
  }

  internal struct CustomDrawingHandlerInfo {
    let customDrawingHandler: CustomDrawingHandler
    let granularity: CustomDrawingGranularity
  }

  internal var customDrawingBackground: [NSAttributedString.Key: CustomDrawingHandlerInfo] = [:]
  internal var customDrawingText: [NSAttributedString.Key: CustomDrawingHandlerInfo] = [:]

  public func registerCustomDrawing(customAttribute: NSAttributedString.Key, layer: CustomDrawingLayer, granularity: CustomDrawingGranularity, handler: @escaping CustomDrawingHandler) throws {
    switch layer {
    case .text:
      customDrawingText[customAttribute] = CustomDrawingHandlerInfo(customDrawingHandler: handler, granularity: granularity)
    case .background:
      customDrawingBackground[customAttribute] = CustomDrawingHandlerInfo(customDrawingHandler: handler, granularity: granularity)
    }
  }

  // MARK: - Other public API

  /// Dispatches a command, running its handlers.
  /// - Parameters:
  ///   - type: The command type (name).
  ///   - payload: The payload required by this command. Some commands may not require a payload.
  /// - Returns: true if the handler should intercept the command (i.e. stop running other handlers); false if other handlers should run.
  @discardableResult
  public func dispatchCommand(type: CommandType, payload: Any? = nil) -> Bool {
    return triggerCommandListeners(activeEditor: self, type: type, payload: payload)
  }

  /// Clears the editor and replaces the current EditorState with a new EditorState
  /// - Parameters:
  ///   - editor: The editor to clear
  ///   - pendingEditorState: A new pending EditorState to replace whatever is already there.
  @objc public func resetEditor(pendingEditorState newEditorState: EditorState? = nil) {

    cloneNotNeeded.removeAll()
    dirtyType = (pendingEditorState != nil) ? .fullReconcile : .noDirtyNodes
    dirtyNodes = [:]
    if let newEditorState, let pendingEditorState {
      pendingEditorState.nodeMap = newEditorState.nodeMap
      pendingEditorState.selection = newEditorState.selection
    } else if let newEditorState {
      pendingEditorState = newEditorState
    } else {
      pendingEditorState = nil
    }
    compositionKey = nil
    editorState = EditorState()

    rangeCache = [:]
    rangeCache[kRootNodeKey] = RangeCacheItem()

    if let textStorage = frontend?.textStorage {
      let oldMode = textStorage.mode
      textStorage.mode = .controllerMode
      textStorage.setAttributedString(NSAttributedString(string: ""))
      textStorage.mode = oldMode
    }

    for (key, value) in decoratorCache {
      switch value {
      case .cachedView(let view):
        view.removeFromSuperview()
      case .unmountedCachedView(let view):
        view.removeFromSuperview()
      case .needsDecorating(let view):
        view.removeFromSuperview()
      case .needsCreation:
        break
      }
      decoratorCache.removeValue(forKey: key)
    }

    if let pendingEditorState {
      for (_, node) in pendingEditorState.nodeMap {
        node.didMoveTo(newEditor: self)
      }
      try? updateWithCustomBehaviour(mode: UpdateBehaviourModificationMode(suppressReconcilingSelection: false, suppressSanityCheck: true, markedTextOperation: nil, skipTransforms: true, allowUpdateWithoutTextStorage: false)) {}
    } else {
      // create a default paragraph node here
      try? updateWithCustomBehaviour(mode: UpdateBehaviourModificationMode(suppressReconcilingSelection: true, suppressSanityCheck: true, markedTextOperation: nil, skipTransforms: true, allowUpdateWithoutTextStorage: true)) {
        guard let root = getRoot() else { return }
        if root.getFirstChild() == nil {
          let paragraph = createParagraphNode()
          try root.append([paragraph])
          let selection = try getSelection()
          if selection != nil {
            try paragraph.select(anchorOffset: nil, focusOffset: nil)
            if let selection = selection as? RangeSelection {
              selection.clearFormat()
            }
          }
        }
      }
    }
  }

  internal func resetReconciler(pendingEditorState: EditorState) {
    resetEditor(pendingEditorState: pendingEditorState)
  }

  /// Returns the current theme
  /// - Returns: The theme that was passed in when this Editor was created.
  public func getTheme() -> Theme {
    theme
  }

  /// Returns if the user is currently entering multi-stage character input
  /// - Returns: true if the user is currently entering multi-stage character input (aka Marked Text, aka Composition)
  public func isComposing() -> Bool {
    return compositionKey != nil
  }

  public func isTextViewEmpty() -> Bool {
    return frontend?.isEmpty ?? true
  }

  public func clearEditor() throws {
    resetEditor(pendingEditorState: nil)
    dispatchCommand(type: .clearEditor)
  }

  // MARK: - Selection

  internal func getNativeSelection() -> NativeSelection {
    return frontend?.nativeSelection ?? NativeSelection()
  }

  internal func moveNativeSelection(type: NativeSelectionModificationType, direction: UITextStorageDirection, granularity: UITextGranularity) {
    frontend?.moveNativeSelection(type: type, direction: direction, granularity: granularity)
  }

  // MARK: - Internal

  public func setEditorState(_ newEditorState: EditorState) throws {
    // If we already have a pending editor state, modify that one. Otherwise, if we're inside an update block, the previous pending editor state
    // will remain attached to the thread as activeEditorState, and things like getLatest won't work right.
    if let pendingEditorState {
      pendingEditorState.nodeMap = newEditorState.nodeMap
      pendingEditorState.selection = newEditorState.selection
    } else {
      pendingEditorState = newEditorState
    }

    dirtyType = .fullReconcile
    cloneNotNeeded = Set()
    if compositionKey != nil {
      if let frontend, frontend.isFirstResponder {
        frontend.unmarkTextWithoutUpdate()
      }
      compositionKey = nil
    }

    try beginUpdate({}, mode: UpdateBehaviourModificationMode())
  }

  internal func testing_getPendingEditorState() -> EditorState? {
    pendingEditorState
  }

  internal func focus(callbackFn: (() -> Void)?) throws {
    try update {
      let selection = try getSelection()
      guard let rootNode = getRoot() else { return }

      if let selection {
        // Marking the selection dirty will force the selection back to it
        selection.dirty = true
      } else if rootNode.children.count != 0 {
        try rootNode.selectEnd()
      }
    }
  }

  // MARK: - Decorators

  internal func frontendDidUnattachView() {
    self.log(.editor, .verbose)
    unmountDecoratorSubviewsIfNecessary()
  }

  internal func frontendDidAttachView() {
    self.log(.editor, .verbose)
    mountDecoratorSubviewsIfNecessary()
  }

  var isMounting = false
  internal func mountDecoratorSubviewsIfNecessary() {
    if isMounting {
      return
    }
    isMounting = true
    defer {
      isMounting = false
    }

    guard let superview = frontend?.viewForDecoratorSubviews else {
      self.log(.editor, .verbose, "No view for mounting decorator subviews.")
      return
    }
    try? self.read {
      for (nodeKey, decoratorCacheItem) in decoratorCache {
        switch decoratorCacheItem {
        case .needsCreation:
          guard let view = decoratorView(forKey: nodeKey, createIfNecessary: true), let node = getNodeByKey(key: nodeKey) as? DecoratorNode else {
            break
          }
          view.isHidden = true // decorators will be hidden until they are layed out by TextKit
          superview.addSubview(view)
          node.decoratorWillAppear(view: view)
          decoratorCache[nodeKey] = DecoratorCacheItem.cachedView(view)
          self.log(.editor, .verbose, "needsCreation -> cached. Key \(nodeKey). Frame \(view.frame). Superview \(String(describing: view.superview))")
        case .cachedView(let view):
          // This shouldn't be needed if our appear/disappear logic is perfect, but it turns out we do currently need this.
          superview.addSubview(view)
          self.log(.editor, .verbose, "no-op, already cached. Key \(nodeKey). Frame \(view.frame). Superview \(String(describing: view.superview))")
        case .unmountedCachedView(let view):
          view.isHidden = true // decorators will be hidden until they are layed out by TextKit
          superview.addSubview(view)
          if let node = getNodeByKey(key: nodeKey) as? DecoratorNode {
            node.decoratorWillAppear(view: view)
          }
          decoratorCache[nodeKey] = DecoratorCacheItem.cachedView(view)
          self.log(.editor, .verbose, "unmounted -> cached. Key \(nodeKey). Frame \(view.frame). Superview \(String(describing: view.superview))")
        case .needsDecorating(let view):
          superview.addSubview(view)
          decoratorCache[nodeKey] = DecoratorCacheItem.cachedView(view)
          if let node = getNodeByKey(key: nodeKey) as? DecoratorNode {
            node.decorate(view: view)
          }
          if let rangeCacheItem = rangeCache[nodeKey] {
            // required so that TextKit does the new size calculation, and correctly hides or unhides the view
            frontend?.layoutManager.invalidateLayout(forCharacterRange: rangeCacheItem.range, actualCharacterRange: nil)
          }
        }
      }
    }
  }

  internal func unmountDecoratorSubviewsIfNecessary() {
    try? self.read {
      for (nodeKey, decoratorCacheItem) in decoratorCache {
        switch decoratorCacheItem {
        case .needsCreation:
          break
        case .cachedView(let view):
          view.removeFromSuperview()
          if let node = getNodeByKey(key: nodeKey) as? DecoratorNode {
            node.decoratorDidDisappear(view: view)
            decoratorCache[nodeKey] = DecoratorCacheItem.unmountedCachedView(view)
          } else {
            decoratorCache[nodeKey] = nil
          }
        case .unmountedCachedView:
          break
        case .needsDecorating(let view):
          view.removeFromSuperview()
          if let node = getNodeByKey(key: nodeKey) as? DecoratorNode {
            node.decoratorDidDisappear(view: view)
            decoratorCache[nodeKey] = DecoratorCacheItem.unmountedCachedView(view)
          } else {
            decoratorCache[nodeKey] = nil
          }
        }
      }
    }
  }

  // MARK: - Manipulating the editor state

  var isUpdating = false
  private func beginUpdate(_ closure: () throws -> Void, mode: UpdateBehaviourModificationMode) throws {
    var editorStateWasCloned = false

    if pendingEditorState == nil {
      pendingEditorState = EditorState(editorState)
      editorStateWasCloned = true
    }

    if infiniteUpdateLoopCount > Editor.maxUpdateCount {
      throw LexicalError.invariantViolation("Maximum update loop met")
    }

    defer {
      infiniteUpdateLoopCount = 0
      isRecoveringFromError = false
    }

    guard let pendingEditorState else {
      return
    }

    let isInsideNestedEditorBlock = (isEditorPresentInUpdateStack(self)) && !isReadOnlyMode()

    try runWithStateLexicalScopeProperties(activeEditor: self, activeEditorState: pendingEditorState, readOnlyMode: false) {
      let previouslyUpdating = self.isUpdating
      self.isUpdating = true

      if editorStateWasCloned {
        pendingEditorState.selection = try createSelection(editor: self)
      }

      do {
        try closure()

        // Need to do this here, in case pendingEditorState was replaced or manipulated inside the closure.
        guard let pendingEditorState = self.pendingEditorState else {
          self.isUpdating = previouslyUpdating
          return
        }

        if isInsideNestedEditorBlock {
          self.isUpdating = previouslyUpdating
          return
        }

        if dirtyType != .noDirtyNodes {
          try normalizeAllDirtyTextNodes(editorState: pendingEditorState)

          if !mode.skipTransforms {
            try applyAllTransforms()
          }
        }

        if mode.allowUpdateWithoutTextStorage && textStorage == nil {
          // we want to leave the pending editor state as pending here; it will be reconciled when a text storage is attached
          self.isUpdating = previouslyUpdating
          return
        }

        if !headless {
          try Reconciler.updateEditorState(currentEditorState: editorState, pendingEditorState: pendingEditorState, editor: self, shouldReconcileSelection: !mode.suppressReconcilingSelection, markedTextOperation: mode.markedTextOperation)
        }
        self.isUpdating = previouslyUpdating
        garbageCollectDetachedNodes(prevEditorState: editorState, editorState: pendingEditorState, dirtyLeaves: dirtyNodes)
      } catch {
        triggerErrorListeners(
          activeEditor: self,
          activeEditorState: pendingEditorState,
          previousEditorState: editorState,
          error: error)
        isRecoveringFromError = true
        resetEditor(pendingEditorState: editorState)
        try beginUpdate({}, mode: UpdateBehaviourModificationMode())
        self.isUpdating = previouslyUpdating
        return
      }

      if let pendingSelection = pendingEditorState.selection as? RangeSelection {
        let anchor = pendingEditorState.nodeMap[pendingSelection.anchor.key]
        let focus = pendingEditorState.nodeMap[pendingSelection.focus.key]
        if anchor == nil || focus == nil {
          let errorString =
            """
        updateEditor: selection has been lost because the previously selected nodes have been removed and
        selection wasn't moved to another node. Ensure selection changes after removing/replacing a selected node.
        """
          throw LexicalError.invariantViolation(errorString)
        }
      } else if let pendingSelection = pendingEditorState.selection as? NodeSelection {
        if pendingSelection.nodes.isEmpty {
          pendingEditorState.selection = nil
        }
      }

      triggerUpdateListeners(activeEditor: self, activeEditorState: pendingEditorState, previousEditorState: editorState, dirtyNodes: dirtyNodes)
      try triggerTextContentListeners(activeEditor: self, activeEditorState: pendingEditorState, previousEditorState: editorState)

      editorState = pendingEditorState
      self.pendingEditorState = nil
      dirtyNodes.removeAll()
      dirtyType = .noDirtyNodes
      cloneNotNeeded.removeAll()

      mountDecoratorSubviewsIfNecessary()
    }

    frontend?.isUpdatingNativeSelection = false

    if featureFlags.reconcilerSanityCheck && !mode.suppressSanityCheck && compositionKey == nil, let frontend {
      do {
        try performReconcilerSanityCheck(editor: self, expectedOutput: frontend.textStorage)
      } catch LexicalError.sanityCheck(errorMessage: let errorMessage, textViewText: let textViewText, fullReconcileText: let fullReconcileText) {
        frontend.presentDeveloperFacingError(message: "\(errorMessage)\n\nIn text view:\n```\n\(textViewText)\n```\n\nFull reconcile:\n```\n\(fullReconcileText)\n```")
        if !isRecoveringFromError {
          isRecoveringFromError = true
          resetReconciler(pendingEditorState: pendingEditorState)
          try beginUpdate({}, mode: UpdateBehaviourModificationMode())
        } else {
          fatalError("Unreconcileable text entered into editor")
        }
      }
    }
  }

  private func beginRead(_ closure: () throws -> Void) throws {
    try runWithStateLexicalScopeProperties(activeEditor: self, activeEditorState: getActiveEditorState() ?? editorState, readOnlyMode: true, closure: closure)
  }

  // There are some cases (mainly related to non-controlled mode and/or UIKit's selection handling) where we
  // want to run an update but not to do everything that is done within an update block. This is definitely for
  // internal Lexical use only, and should only be done if safety can be guaranteed, i.e. the caller of
  // such an update must guarantee that the EditorState will not be left in an inconsistent state when they are finished.
  internal func updateWithCustomBehaviour(mode: UpdateBehaviourModificationMode, _ closure: () throws -> Void) throws {
    try beginUpdate(closure, mode: mode)
  }

  internal func normalizeAllDirtyTextNodes(editorState: EditorState) throws {
    guard let activeEditorState = getActiveEditorState() else {
      throw LexicalError.invariantViolation("Cannot normalize nodes without an active editor state")
    }

    for (nodeKey, _) in dirtyNodes {
      guard let node = editorState.nodeMap[nodeKey], isTextNode(node) else {
        continue
      }

      guard activeEditorState.nodeMap[nodeKey] != nil else {
        throw LexicalError.invariantViolation("TextNode \(nodeKey) was not in active editor state during text normalization")
      }

      if let textNode = node as? TextNode, textNode.isSimpleText() && !textNode.isUnmergeable() {
        try TextNode.normalizeTextNode(textNode: textNode)
      }
    }
  }

  internal func applyAllTransforms() throws {
    guard let editorState = getActiveEditorState(), let rootNode = editorState.getRootNode() else {
      throw LexicalError.invariantViolation(
        "Attempted to apply transforms on unmounted tree or outside an active editor")
    }

    // This code needs to be a little different from the web version. We do not explicitly
    // track the difference between leaf and element nodes by key, so our code is different.
    //
    // The two main differences are:
    //   1. While we prioritize doing child transforms first, child nodes that generate element nodes
    //      are not processed until the the next round of transforms are performed.
    //   2. As a result, the two loops in the web reference are collapsed and so the need for
    //      many of the utility methods are as well. Some temporaries that were necessary
    //      no longer are. Structurally, the code is different but the effect should be the same,
    //      though we are less tolerant of deeply nested transform loops as a result.

    // Due to our sorting scheme, this sentinel value is needed to ensure that leaf nodes always
    // take priority over element nodes (even if that element node has no children.)
    let isLeafNodeSigil = -1
    var nodeChildrenCounts = [NodeKey: Int]()

    let originalDirtyNodes = dirtyNodes

    while !dirtyNodes.isEmpty {
      if infiniteUpdateLoopCount >= Editor.maxUpdateCount {
        throw LexicalError.invariantViolation("Update loop exceeded maximum of \(Editor.maxUpdateCount)")
      }

      let nodesToProcess = dirtyNodes.keys
        .filter { dirtyNodes[$0] == .userInitiated }
        .sorted { leftNodeKey, rightNodeKey in
          let leftNodeChildCount: Int
          let rightNodeChildCount: Int

          // To try to avoid many failed casts we try to keep a hash lookup of the child count
          if let count = nodeChildrenCounts[leftNodeKey] {
            leftNodeChildCount = count
          } else if let leftNode = getNodeByKey(key: leftNodeKey) as? ElementNode {
            leftNodeChildCount = leftNode.getChildrenSize()
          } else {
            leftNodeChildCount = isLeafNodeSigil
          }

          if let count = nodeChildrenCounts[rightNodeKey] {
            rightNodeChildCount = count
          } else if let leftNode = getNodeByKey(key: rightNodeKey) as? ElementNode {
            rightNodeChildCount = leftNode.getChildrenSize()
          } else {
            rightNodeChildCount = isLeafNodeSigil
          }

          nodeChildrenCounts[leftNodeKey] = leftNodeChildCount
          nodeChildrenCounts[rightNodeKey] = rightNodeChildCount

          return leftNodeChildCount < rightNodeChildCount
        }

      dirtyNodes.removeAll(keepingCapacity: true)

      for nodeKey in nodesToProcess where nodeKey != compositionKey && nodeKey != rootNode.key {
        guard
          let node = getNodeByKey(key: nodeKey),
          let transforms = nodeTransforms[node.type],
          node.isAttached()
        else {
          continue
        }

        if isTextNode(node) {
          if let textNode = node as? TextNode, textNode.isSimpleText() && !textNode.isUnmergeable() {
            try TextNode.normalizeTextNode(textNode: textNode)
          }
        }

        for (_, transform) in transforms where node.isAttached() {
          try transform(node)
        }
      }

      // As any number of changes may have been made as a result of the transforms, we need to
      // update our lookup as a result.
      nodeChildrenCounts.removeAll()

      infiniteUpdateLoopCount += 1
    }

    dirtyNodes = originalDirtyNodes
  }

  // MARK: Node Transforms

  /// Adds a Transform, allowing you to make changes in response to an EditorState update.
  /// - Parameters:
  ///   - nodeType: The node type you want to listen for changes to
  ///   - transform: Code to run allowing you to further modify the node
  /// - Returns: A closure allowing you to remove the transform.
  ///
  /// Transforms are executed before reconciliation.
  ///
  /// > Important: In most cases, it is possible to achieve the same or very similar result through an update listener
  /// followed by an update. This is highly discouraged as it triggers an additional reconciliation pass. Additionally, each
  /// cycle creates a brand new EditorState object which can interfere with plugins like HistoryPlugin (undo-redo)
  /// if not handled correctly.
  public func addNodeTransform(nodeType: NodeType, transform: @escaping NodeTransform) -> () -> Void {
    // NB: In the web code, closures can be compared for identity but in Swift, closures are
    //     by design not Equatable. Therefore, we generate a tag for each closure passed in
    //     and use that for our removal/cleanup logic.

    transformCounter += 1

    var transforms: [(Int, NodeTransform)]
    let id = transformCounter

    if let existingTransforms = nodeTransforms[nodeType] {
      transforms = existingTransforms
    } else {
      transforms = [(Int, NodeTransform)]()
    }

    transforms.append((id, transform))
    nodeTransforms[nodeType] = transforms

    return { [weak self] in
      if let strongSelf = self, var transforms = strongSelf.nodeTransforms[nodeType] {
        transforms.removeAll(where: { $0.0 == id })
        strongSelf.nodeTransforms[nodeType] = transforms
      }
    }
  }

  internal func parseEditorState(json: Data) throws -> EditorState {
    let previousActiveEditorState = self.editorState
    let previousPendingEditorState = self.pendingEditorState
    let previousDirtyNodes = self.dirtyNodes
    let previousDirtyType = self.dirtyType
    let previousRangeCache = self.rangeCache
    let previousCloneNotNeeded = self.cloneNotNeeded
    let previousNormalizedNodes = self.normalizedNodes
    let previousHeadless = self.headless

    let editorState = EditorState()

    self.dirtyNodes = [:]
    self.dirtyType = .noDirtyNodes
    self.cloneNotNeeded = Set()
    self.rangeCache = [kRootNodeKey: RangeCacheItem()]
    self.normalizedNodes = Set()
    self.editorState = editorState
    self.pendingEditorState = nil
    self.headless = true

    defer {
      self.dirtyNodes = previousDirtyNodes
      self.dirtyType = previousDirtyType
      self.cloneNotNeeded = previousCloneNotNeeded
      self.rangeCache = previousRangeCache
      self.normalizedNodes = previousNormalizedNodes
      self.editorState = previousActiveEditorState
      self.pendingEditorState = previousPendingEditorState
      self.headless = previousHeadless
    }

    try self.beginUpdate({
      let serializedEditorState = try JSONDecoder().decode(SerializedEditorState.self, from: json)

      guard let serializedRootNode = serializedEditorState.rootNode, let rootNode = getRoot() else {
        throw LexicalError.internal("Failed to decode RootNode")
      }

      try rootNode.append(serializedRootNode.getChildren())
      try rootNode.setDirection(direction: serializedRootNode.direction)
    }, mode: UpdateBehaviourModificationMode(suppressReconcilingSelection: true, suppressSanityCheck: true, markedTextOperation: nil, skipTransforms: true, allowUpdateWithoutTextStorage: false))

    return self.editorState
  }
}

internal enum NativeSelectionModificationType {
  case move
  case extend
}

internal struct UpdateBehaviourModificationMode {
  let markedTextOperation: MarkedTextOperation?
  let skipTransforms: Bool
  let suppressReconcilingSelection: Bool
  let suppressSanityCheck: Bool
  let allowUpdateWithoutTextStorage: Bool

  internal init(suppressReconcilingSelection: Bool = false,
                suppressSanityCheck: Bool = false,
                markedTextOperation: MarkedTextOperation? = nil,
                skipTransforms: Bool = false,
                allowUpdateWithoutTextStorage: Bool = false) {
    self.suppressReconcilingSelection = suppressReconcilingSelection
    self.suppressSanityCheck = suppressSanityCheck
    self.markedTextOperation = markedTextOperation
    self.skipTransforms = skipTransforms
    self.allowUpdateWithoutTextStorage = allowUpdateWithoutTextStorage
  }
}


/*
 * 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.
 */

import Foundation
internal let kRootNodeKey = "root"

/**
 The Lexical data model.

 An EditorState contains nodes and a selection. It can be serialized to and from JSON, and passed around between Lexical ``Editor`` instances.
 */
public class EditorState: NSObject {

  internal var nodeMap: [NodeKey: Node] = [:]
  public var selection: BaseSelection?

  override init() {
    let rootNode = RootNode()
    nodeMap[kRootNodeKey] = rootNode
  }

  init(_ editorState: EditorState) {
    nodeMap = editorState.nodeMap
  }

  /// Returns the root node for this EditorState, if one is set.
  public func getRootNode() -> RootNode? {
    return nodeMap[kRootNodeKey] as? RootNode
  }

  /// Accessor function for the node map (i.e. the dictionary of key -> node object).
  public func getNodeMap() -> [NodeKey: Node] {
    return nodeMap
  }

  /// Allows you to interrogate the contents of this EditorState without having to attach it to an Editor.
  public func read<V>(closure: () throws -> V) throws -> V? {
    return try beginRead(activeEditorState: self, closure: closure)
  }

  private func beginRead<V>(
    activeEditorState: EditorState,
    closure: () throws -> V
  ) throws -> V? {
    var result: V?
    try runWithStateLexicalScopeProperties(activeEditor: nil, activeEditorState: activeEditorState, readOnlyMode: true) {
      result = try closure()
    }
    return result
  }

  /// Copies this EditorState, optionally adding a new selection while doing so.
  public func clone(selection: RangeSelection?) -> EditorState {
    let editorState = EditorState(self)

    if let selection {
      editorState.selection = selection.clone() as? RangeSelection
    } else {
      editorState.selection = self.selection?.clone() as? RangeSelection
    }

    return editorState
  }

  public static func ==(lhs: EditorState, rhs: EditorState) -> Bool {
    if lhs.nodeMap.count != rhs.nodeMap.count {
      return false
    }

    var isEqual = true

    rhs.nodeMap.forEach { element in
      isEqual = isEqual && (lhs.nodeMap[element.key] == element.value)
    }

    let selectionEqual: Bool
    if let lhsSelection = lhs.selection, let rhsSelection = rhs.selection {
      selectionEqual = lhsSelection.isSelection(rhsSelection)
    } else if lhs.selection == nil && rhs.selection == nil {
      selectionEqual = true
    } else {
      selectionEqual = false
    }

    return isEqual && selectionEqual
  }

  static func createEmptyEditorState() -> EditorState {
    EditorState()
  }

  /**
   Returns a JSON string representing this EditorState.

   The JSON string is designed to be interoperable with Lexical JavaScript (subject to the individual node classes using matching keys).
   */
  public func toJSON() throws -> String {
    let string: String? = try read {
      guard let rootNode = getRootNode() else {
        throw LexicalError.invariantViolation("Could not get RootNode")
      }
      let persistedEditorState = SerializedEditorState(rootNode: rootNode)
      let encodedData = try JSONEncoder().encode(persistedEditorState)
      guard let jsonString = String(data: encodedData, encoding: .utf8) else { return "" }
      return jsonString
    }
    if let string {
      return string
    }
    throw LexicalError.invariantViolation("Expected string")
  }

  /**
   Creates a new EditorState from a JSON string.

   This function requires an ``Editor`` to be passed in, so that the list of registered node classes and plugins can be used when deserializing the JSON.
   The newly created EditorState is not added to the Editor; it is expected that the API consumer will call ``Editor/setEditorState(_:)`` if that is desired.
   */
  public static func fromJSON(json: String, editor: Editor) throws -> EditorState {
    guard let stateData = json.data(using: .utf8) else {
      throw LexicalError.internal("Could not generate data from string JSON state")
    }

    return try editor.parseEditorState(json: stateData)
  }
}


/*
 * 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.
 */

import Foundation

public enum LexicalError: Error {
  case `internal`(String)
  case invariantViolation(String)
  case sanityCheck(errorMessage: String, textViewText: String, fullReconcileText: String)
  case reconciler(String)
  case rangeCacheSearch(String)
}


/*
 * 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.
 */

import Foundation
import UIKit
// This function is analagous to the parts of onBeforeInput() where inputType == 'insertText'.
// However, on iOS, we are assuming that `shouldPreventDefaultAndInsertText()` has already been checked
// before calling onInsertTextFromUITextView().

internal func onInsertTextFromUITextView(text: String, editor: Editor, updateMode: UpdateBehaviourModificationMode = UpdateBehaviourModificationMode()) throws {
  try editor.updateWithCustomBehaviour(mode: updateMode) {
    guard let selection = try getSelection() as? RangeSelection else {
      // should have a range selection if UITextView is first responder
      editor.log(.UITextView, .error, "Expected a selection here")
      return
    }

    if let markedTextOperation = updateMode.markedTextOperation, markedTextOperation.createMarkedText == true {
      // Here we special case STARTING or UPDATING a marked text operation.
      try selection.applySelectionRange(markedTextOperation.selectionRangeToReplace, affinity: .forward)
    } else if let markedRange = editor.getNativeSelection().markedRange {
      // Here we special case ENDING a marked text operation by replacing all the marked text with the incoming text.
      // This is usually used by hardware keyboards e.g. when typing e-acute. Software keyboards such as Japanese
      // do not seem to use this way of ending marked text.
      try selection.applySelectionRange(markedRange, affinity: .forward)
    }

    if text == "\n" || text == "\u{2029}" {
      try selection.insertParagraph()
    } else if text == "\u{2028}" {
      try selection.insertLineBreak(selectStart: false)
    } else {
      try selection.insertText(text)
    }
  }
}

internal func onInsertLineBreakFromUITextView(editor: Editor) throws {
  guard getActiveEditor() != nil, let selection = try getSelection() as? RangeSelection else {
    throw LexicalError.invariantViolation("No editor or selection")
  }
  try selection.insertLineBreak(selectStart: false)
}

internal func onInsertParagraphFromUITextView(editor: Editor) throws {
  guard getActiveEditor() != nil, let selection = try getSelection() as? RangeSelection else {
    throw LexicalError.invariantViolation("No editor or selection")
  }
  try selection.insertParagraph()
}

internal func onRemoveTextFromUITextView(editor: Editor) throws {
  guard getActiveEditor() != nil, let selection = try getSelection() as? RangeSelection else {
    throw LexicalError.invariantViolation("No editor or selection")
  }
  try selection.removeText()

  editor.frontend?.showPlaceholderText()
}

internal func onDeleteBackwardsFromUITextView(editor: Editor) throws {
  guard let editor = getActiveEditor(), let selection = try getSelection() as? RangeSelection else {
    throw LexicalError.invariantViolation("No editor or selection")
  }

  try selection.deleteCharacter(isBackwards: true)

  editor.frontend?.showPlaceholderText()
}

internal func onDeleteWordFromUITextView(editor: Editor) throws {
  guard getActiveEditor() != nil, let selection = try getSelection() as? RangeSelection else {
    throw LexicalError.invariantViolation("No editor or selection")
  }

  try selection.deleteWord(isBackwards: true)

  editor.frontend?.showPlaceholderText()
}

internal func onDeleteLineFromUITextView(editor: Editor) throws {
  guard getActiveEditor() != nil, let selection = try getSelection() as? RangeSelection else {
    throw LexicalError.invariantViolation("No editor or selection")
  }

  try selection.deleteLine(isBackwards: true)

  editor.frontend?.showPlaceholderText()
}

internal func onFormatTextFromUITextView(editor: Editor, type: TextFormatType) throws {
  try updateTextFormat(type: type, editor: editor)
}

internal func onCopyFromUITextView(editor: Editor, pasteboard: UIPasteboard) throws {
  guard getActiveEditor() != nil, let selection = try getSelection() else {
    throw LexicalError.invariantViolation("No editor or selection")
  }
  try setPasteboard(selection: selection, pasteboard: pasteboard)
}

internal func onCutFromUITextView(editor: Editor, pasteboard: UIPasteboard) throws {
  guard getActiveEditor() != nil, let selection = try getSelection() as? RangeSelection else {
    throw LexicalError.invariantViolation("No editor or selection")
  }
  try setPasteboard(selection: selection, pasteboard: pasteboard)
  try selection.removeText()

  editor.frontend?.showPlaceholderText()
}

internal func onPasteFromUITextView(editor: Editor, pasteboard: UIPasteboard) throws {
  guard getActiveEditor() != nil, let selection = try getSelection() as? RangeSelection else {
    throw LexicalError.invariantViolation("No editor or selection")
  }

  try insertDataTransferForRichText(selection: selection, pasteboard: pasteboard)

  editor.frontend?.showPlaceholderText()
}

public func shouldInsertTextAfterOrBeforeTextNode(selection: RangeSelection, node: TextNode) -> Bool {
  var shouldInsertTextBefore = false
  var shouldInsertTextAfter = false

  if node.isSegmented() {
    return true
  }

  if !selection.isCollapsed() {
    return true
  }

  let offset = selection.anchor.offset

  shouldInsertTextBefore = offset == 0 && checkIfTokenOrCanTextBeInserted(node: node)

  shouldInsertTextAfter = node.getTextContentSize() == offset &&
    checkIfTokenOrCanTextBeInserted(node: node)

  return shouldInsertTextBefore || shouldInsertTextAfter
}

func checkIfTokenOrCanTextBeInserted(node: TextNode) -> Bool {
  let isToken = node.isToken()
  let parent = node.getParent()

  if let parent {
    return !parent.canInsertTextBefore() || !node.canInsertTextBefore() || isToken
  }

  return !node.canInsertTextBefore() || isToken
}

// triggered by selection change event from the UITextView
internal func onSelectionChange(editor: Editor) {
  do {
    try editor.updateWithCustomBehaviour(mode: UpdateBehaviourModificationMode(suppressReconcilingSelection: true, suppressSanityCheck: true)) {
      let nativeSelection = editor.getNativeSelection()
      guard let editorState = getActiveEditorState() else {
        return
      }
      if !(try getSelection() is RangeSelection) {
        guard let newSelection = RangeSelection(nativeSelection: nativeSelection) else {
          return
        }
        editorState.selection = newSelection
      }

      guard let lexicalSelection = try getSelection() as? RangeSelection else {
        return // we should have a range selection by now, so this is unexpected
      }

      try lexicalSelection.applyNativeSelection(nativeSelection)

      switch lexicalSelection.anchor.type {
      case .text:
        guard let anchorNode = try lexicalSelection.anchor.getNode() as? TextNode else { break }
        lexicalSelection.format = anchorNode.getFormat()
      case .element:
        lexicalSelection.format = TextFormat()
      default:
        break
      }
      editor.dispatchCommand(type: .selectionChange, payload: nil)
    }
  } catch {
    // log error "change selection: failed to update lexical selection"
  }
}

internal func handleIndentAndOutdent(insertTab: (Node) -> Void, indentOrOutdent: (ElementNode) -> Void) throws {
  guard getActiveEditor() != nil, let selection = try getSelection() else {
    throw LexicalError.invariantViolation("No editor or selection")
  }
  var alreadyHandled: Set<NodeKey> = Set()
  let nodes = try selection.getNodes()

  for node in nodes {
    let key = node.getKey()
    if alreadyHandled.contains(key) { continue }
    let parentBlock = try getNearestBlockElementAncestorOrThrow(startNode: node)
    let parentKey = parentBlock.getKey()
    if parentBlock.canInsertTab() {
      insertTab(parentBlock)
      alreadyHandled.insert(parentKey)
    } else if parentBlock.canIndent() && !alreadyHandled.contains(parentKey) {
      alreadyHandled.insert(parentKey)
      indentOrOutdent(parentBlock)
    }
  }
}

public func registerRichText(editor: Editor) {

  _ = editor.registerCommand(type: .insertLineBreak, listener: { [weak editor] payload in
    guard let editor else { return false }
    do {
      try onInsertLineBreakFromUITextView(editor: editor)
      return true
    } catch {
      print("\(error)")
    }
    return true
  })

  _ = editor.registerCommand(type: .deleteCharacter, listener: { [weak editor] payload in
    guard let editor else { return false }
    do {
      try onDeleteBackwardsFromUITextView(editor: editor)
      return true
    } catch {
      print("\(error)")
    }
    return true
  })

  _ = editor.registerCommand(type: .deleteWord, listener: { [weak editor] payload in
    guard let editor else { return false }
    do {
      try onDeleteWordFromUITextView(editor: editor)
      return true
    } catch {
      print("\(error)")
    }
    return true
  })

  _ = editor.registerCommand(type: .deleteLine, listener: { [weak editor] payload in
    guard let editor else { return false }
    do {
      try onDeleteLineFromUITextView(editor: editor)
      return true
    } catch {
      print("\(error)")
    }
    return true
  })

  _ = editor.registerCommand(type: .insertText, listener: { [weak editor] payload in
    guard let editor else { return false }
    do {
      guard let text = payload as? String else {
        editor.log(.TextView, .warning, "insertText missing payload")
        return false
      }

      try onInsertTextFromUITextView(text: text, editor: editor)
      return true
    } catch {
      editor.log(.TextView, .error, "Exception in insertText; \(String(describing: error))")
    }
    return true
  })

  _ = editor.registerCommand(type: .insertParagraph, listener: { [weak editor] payload in
    guard let editor else { return false }
    do {
      try onInsertParagraphFromUITextView(editor: editor)
      return true
    } catch {
      print("\(error)")
    }
    return true
  })

  _ = editor.registerCommand(type: .removeText, listener: { [weak editor] payload in
    guard let editor else { return false }
    do {
      try onRemoveTextFromUITextView(editor: editor)
      return true
    } catch {
      print("\(error)")
    }
    return true
  })

  _ = editor.registerCommand(type: .formatText, listener: { [weak editor] payload in
    guard let editor else { return false }
    do {
      guard let text = payload as? TextFormatType else { return false }

      try onFormatTextFromUITextView(editor: editor, type: text)
      return true
    } catch {
      print("\(error)")
    }
    return true
  })

  _ = editor.registerCommand(type: .copy, listener: { [weak editor] payload in
    guard let editor else { return false }
    do {
      guard let text = payload as? UIPasteboard else { return false }

      try onCopyFromUITextView(editor: editor, pasteboard: text)
      return true
    } catch {
      print("\(error)")
    }
    return true
  })

  _ = editor.registerCommand(type: .cut, listener: { [weak editor] payload in
    guard let editor else { return false }
    do {
      guard let text = payload as? UIPasteboard else { return false }

      try onCutFromUITextView(editor: editor, pasteboard: text)
      return true
    } catch {
      print("\(error)")
    }
    return true
  })

  _ = editor.registerCommand(type: .paste, listener: { [weak editor] payload in
    guard let editor else { return false }
    do {
      guard let text = payload as? UIPasteboard else { return false }

      try onPasteFromUITextView(editor: editor, pasteboard: text)
      return true
    } catch {
      print("\(error)")
    }
    return true
  })

  _ = editor.registerCommand(type: .indentContent, listener: { [weak editor] payload in
    guard let editor else { return false }
    do {
      try handleIndentAndOutdent(insertTab: { node in
        editor.dispatchCommand(type: .insertText, payload: "\t")
      }, indentOrOutdent: { elementNode in
        let indent = elementNode.getIndent()
        if indent != 10 {
          _ = try? elementNode.setIndent(indent + 1)
        }
      })
      return true
    } catch {
      print("\(error)")
    }
    return true
  })

  _ = editor.registerCommand(type: .outdentContent, listener: { [weak editor] payload in
    guard let editor else { return false }
    do {
      try handleIndentAndOutdent(insertTab: { node in
        if let node = node as? TextNode {
          let textContent = node.getTextContent()
          if let character = textContent.last {
            if character == "\t" {
              editor.dispatchCommand(type: .deleteCharacter)
            }
          }
        }

        editor.dispatchCommand(type: .insertText, payload: "\t")
      }, indentOrOutdent: { elementNode in
        let indent = elementNode.getIndent()
        if indent != 0 {
          _ = try? elementNode.setIndent(indent - 1)
        }
      })
      return true
    } catch {
      print("\(error)")
    }
    return true
  })
}


/*
 * 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.
 */

import Foundation

@objc public class FeatureFlags: NSObject {
  let reconcilerSanityCheck: Bool
  let proxyTextViewInputDelegate: Bool

  @objc public init(reconcilerSanityCheck: Bool = false, proxyTextViewInputDelegate: Bool = false) {
    self.reconcilerSanityCheck = reconcilerSanityCheck
    self.proxyTextViewInputDelegate = proxyTextViewInputDelegate
    super.init()
  }
}


/*
 * 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.
 */

import Foundation
import UIKit

internal func garbageCollectDetachedDeepChildNodes(
  node: ElementNode,
  parentKey: NodeKey,
  prevNodeMap: [NodeKey: Node],
  nodeMap: [NodeKey: Node],
  dirtyNodes: DirtyNodeMap
) {
  var dirtyNodes = dirtyNodes
  var nodeMap = nodeMap
  let children = node.children

  for childKey in children {
    let child = nodeMap[childKey]

    if let child = child as? ElementNode, child.parent == parentKey {
      garbageCollectDetachedDeepChildNodes(
        node: child,
        parentKey: childKey,
        prevNodeMap: prevNodeMap,
        nodeMap: nodeMap,
        dirtyNodes: dirtyNodes
      )
    }

    if prevNodeMap[childKey] == nil {
      dirtyNodes.removeValue(forKey: childKey)
    }

    nodeMap.removeValue(forKey: childKey)
  }
}

func garbageCollectDetachedNodes(
  prevEditorState: EditorState,
  editorState: EditorState,
  dirtyLeaves: DirtyNodeMap
) {
  let prevNodeMap = prevEditorState.nodeMap
  var nodeMap = editorState.nodeMap
  var dirtyLeaves = dirtyLeaves
  var dirtyElements = DirtyNodeMap()

  for (nodeKey, _) in dirtyLeaves {
    if let node = nodeMap[nodeKey] {
      if node is ElementNode {
        dirtyElements[nodeKey] = .editorInitiated
        continue
      }
      if !node.isAttached() {
        if prevNodeMap[nodeKey] == nil {
          dirtyLeaves.removeValue(forKey: nodeKey)
        }

        nodeMap.removeValue(forKey: nodeKey)
      }
    }
  }

  for (nodeKey, _) in dirtyElements {
    if let node = nodeMap[nodeKey] {
      if !node.isAttached() {
        if let node = node as? ElementNode {
          garbageCollectDetachedDeepChildNodes(node: node, parentKey: nodeKey, prevNodeMap: prevNodeMap, nodeMap: nodeMap, dirtyNodes: dirtyElements)
        }

        if prevNodeMap[nodeKey] == nil {
          dirtyElements.removeValue(forKey: nodeKey)
        }

        nodeMap.removeValue(forKey: nodeKey)
      }
    }
  }

  editorState.nodeMap = nodeMap
}


/*
 * 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.
 */

import Foundation
import UIKit

enum MergeAction {
  case historyMerge
  case historyPush
  case discardHistoryCandidate
}

enum ChangeType {
  case other
  case composingCharacter
  case insertCharacterAfterSelection
  case insertCharacterBeforeSelection
  case deleteCharacterBeforeSelection
  case deleteCharacterAfterSelection
}

public class EditorHistory {
  var editor: Editor
  var externalHistoryState: HistoryState?
  var delay: Int
  var prevChangeTime: Double
  var prevChangeType: ChangeType

  public init(editor: Editor, externalHistoryState: HistoryState, delay: Int = 5) {
    self.editor = editor
    self.externalHistoryState = externalHistoryState
    self.delay = delay
    self.prevChangeTime = 0
    self.prevChangeType = .other
  }

  public func applyChange(
    editorState: EditorState,
    prevEditorState: EditorState,
    dirtyNodes: DirtyNodeMap) {
    let historyState: HistoryState = externalHistoryState ?? createEmptyHistoryState()
    let currentEditorState = historyState.current == nil ? nil : historyState.current?.editorState

    if historyState.current != nil && editorState == currentEditorState {
      return
    }

    if historyState.current == nil {
      historyState.current = HistoryStateEntry(
        editor: editor,
        editorState: prevEditorState,
        undoSelection: prevEditorState.selection?.clone() as? RangeSelection)
    }

    do {
      let mergeAction = try getMergeAction(
        prevEditorState: prevEditorState,
        nextEditorState: editorState,
        currentHistoryEntry: historyState.current,
        dirtyNodes: dirtyNodes)

      if mergeAction == .historyPush {
        if !historyState.redoStack.isEmpty {
          historyState.redoStack = []
          editor.dispatchCommand(type: .canRedo, payload: false)
        }

        if let current = historyState.current, let editor = current.editor {
          historyState.undoStack.append(
            HistoryStateEntry(
              editor: editor,
              editorState: current.editorState,
              undoSelection: prevEditorState.selection?.clone() as? RangeSelection))
        }

        editor.dispatchCommand(type: .canUndo, payload: true)
      } else if mergeAction == .discardHistoryCandidate {
        return
      }

      historyState.current = HistoryStateEntry(
        editor: editor,
        editorState: editorState,
        undoSelection: editorState.selection?.clone() as? RangeSelection)

      externalHistoryState = historyState
    } catch {
      print("Failed to get mergeAction: \(error.localizedDescription)")
    }
  }

  func undo() {
    guard let externalHistoryState,
          externalHistoryState.undoStack.count != 0
    else { return }

    var historyStateEntry = externalHistoryState.undoStack.removeLast()
    if let current = externalHistoryState.current {
      externalHistoryState.redoStack.append(current)
      editor.dispatchCommand(type: .canRedo, payload: true)
    }

    if externalHistoryState.undoStack.count == 0 {
      editor.dispatchCommand(type: .canUndo, payload: false)
    }

    externalHistoryState.current = historyStateEntry
    do {
      if let editor = historyStateEntry.editor,
         let undoSelection = historyStateEntry.undoSelection {
        try editor.setEditorState(historyStateEntry.editorState.clone(selection: undoSelection))
        historyStateEntry.editor = editor
        editor.frontend?.showPlaceholderText()
      }
    } catch {
      editor.log(.other, .warning, "undo: Failed to setEditorState: \(error.localizedDescription)")
    }

    self.externalHistoryState = externalHistoryState
  }

  func redo() {
    guard let externalHistoryState,
          externalHistoryState.redoStack.count != 0
    else { return }

    if let current = externalHistoryState.current {
      externalHistoryState.undoStack.append(current)
      editor.dispatchCommand(type: .canUndo, payload: true)
    }

    let historyStateEntry = externalHistoryState.redoStack.removeLast()
    if externalHistoryState.redoStack.count == 0 {
      editor.dispatchCommand(type: .canRedo, payload: false)
    }

    externalHistoryState.current = historyStateEntry

    do {
      try editor.setEditorState(historyStateEntry.editorState.clone(selection: historyStateEntry.undoSelection))
      editor.frontend?.showPlaceholderText()
    } catch {
      editor.log(.other, .warning, "redo: Failed to setEditorState: \(error.localizedDescription)")
    }

    self.externalHistoryState = externalHistoryState
  }

  public func applyCommand(type: CommandType) {
    if type == .redo {
      redo()
    } else if type == .undo {
      undo()
    } else if type == .clearEditor {
      guard let externalHistoryState else { return }

      clearHistory(historyState: externalHistoryState)
    }
  }

  func getMergeAction(
    prevEditorState: EditorState?,
    nextEditorState: EditorState,
    currentHistoryEntry: HistoryStateEntry?,
    dirtyNodes: DirtyNodeMap
  ) throws -> MergeAction {
    let changeTime = Date().timeIntervalSince1970

    if prevChangeTime == 0 {
      prevChangeTime = Date().timeIntervalSince1970
    }

    let changeType = try getChangeType(
      prevEditorState: prevEditorState,
      nextEditorState: nextEditorState,
      dirtyLeavesSet: dirtyNodes,
      isComposing: editor.isComposing())

    let selection = nextEditorState.selection
    let prevSelection = prevEditorState?.selection
    let hasDirtyNodes = dirtyNodes.count > 0
    if !hasDirtyNodes {
      if prevSelection == nil && selection != nil {
        prevChangeTime = changeTime
        prevChangeType = changeType

        return .historyMerge
      }

      // since we're discarding the candidate, do not cache the prev change time/type
      return .discardHistoryCandidate
    }

    let isSameEditor = currentHistoryEntry == nil || currentHistoryEntry?.editor == self.editor

    if changeType != .other &&
        changeType == prevChangeType &&
        changeTime < prevChangeTime + Double(self.delay) &&
        isSameEditor {
      prevChangeTime = changeTime
      prevChangeType = changeType

      return .historyMerge
    }

    prevChangeTime = changeTime
    prevChangeType = changeType

    return .historyPush
  }
}

public struct HistoryStateEntry {
  var editor: Editor?
  var editorState: EditorState
  var undoSelection: RangeSelection?

  public init(editor: Editor, editorState: EditorState, undoSelection: RangeSelection?) {
    self.editor = editor
    self.editorState = editorState
    self.undoSelection = undoSelection
  }
}

public class HistoryState {
  var current: HistoryStateEntry?
  var redoStack: [HistoryStateEntry] = []
  var undoStack: [HistoryStateEntry] = []

  public init(current: HistoryStateEntry?, redoStack: [HistoryStateEntry], undoStack: [HistoryStateEntry]) {
    self.current = current
    self.redoStack = redoStack
    self.undoStack = undoStack
  }
}

func getDirtyNodes(
  editorState: EditorState,
  dirtyLeavesSet: DirtyNodeMap) -> [Node] {
  let dirtyLeaves = dirtyLeavesSet
  let nodeMap = editorState.nodeMap
  var nodes: [Node] = []

  for (dirtyLeafKey, cause) in dirtyLeaves {
    if cause == .editorInitiated {
      continue
    }

    if let dirtyLeaf = nodeMap[dirtyLeafKey] {
      if dirtyLeaf is TextNode {
        nodes.append(dirtyLeaf)
      }
    }

    if let dirtyElement = nodeMap[dirtyLeafKey] {
      if dirtyElement is ElementNode && !isRootNode(node: dirtyElement) {
        nodes.append(dirtyElement)
      }
    }
  }
  return nodes
}

func getChangeType(
  prevEditorState: EditorState?,
  nextEditorState: EditorState,
  dirtyLeavesSet: DirtyNodeMap,
  isComposing: Bool) throws -> ChangeType {
  if prevEditorState == nil || dirtyLeavesSet.count == 0 {
    return .other
  }

  guard let prevEditorState else { return .other }

  if isComposing {
    return .composingCharacter
  }

  guard let nextSelection = nextEditorState.selection,
        let prevSelection = prevEditorState.selection
  else {
    throw LexicalError.internal("Failed to find selection")
  }

  guard let nextSelection = nextSelection as? RangeSelection,
        let prevSelection = prevSelection as? RangeSelection else {
    return .other
  }

  if !prevSelection.isCollapsed() || !nextSelection.isCollapsed() {
    return .other
  }

  let dirtyNodes = getDirtyNodes(editorState: nextEditorState, dirtyLeavesSet: dirtyLeavesSet)
  if dirtyNodes.count == 0 {
    return .other
  }

  // Catching the case when inserting new text node into an element (e.g. first char in paragraph/list),
  // or after existing node.
  if dirtyNodes.count > 1 {
    let nextNodeMap = nextEditorState.nodeMap

    let prevAnchorNode = nextNodeMap[prevSelection.anchor.key]

    if let nextAnchorNode = nextNodeMap[nextSelection.anchor.key] as? TextNode,
       (prevAnchorNode != nil),
       !prevEditorState.nodeMap.keys.contains(nextAnchorNode.key),
       nextAnchorNode.getTextPartSize() == 1,
       nextSelection.anchor.offset == 1 {
      return .insertCharacterAfterSelection
    }
    return .other
  }

  let nextDirtyNode = dirtyNodes[0]
  let prevDirtyNode = prevEditorState.nodeMap[nextDirtyNode.key]

  if !isTextNode(prevDirtyNode) || !isTextNode(nextDirtyNode) {
    return .other
  }

  guard
    let prevDirtyNode = prevDirtyNode as? TextNode,
    let nextDirtyNode = nextDirtyNode as? TextNode
  else {
    throw LexicalError.internal("prev/nextDirtyNode is not TextNode")
  }

  if prevDirtyNode.mode != nextDirtyNode.mode {
    return .other
  }

  // we don't want the text from latest node
  let prevText = prevDirtyNode.getText_dangerousPropertyAccess()
  let nextText = nextDirtyNode.getText_dangerousPropertyAccess()
  if prevText == nextText {
    return .other
  }

  let nextAnchor = nextSelection.anchor
  let prevAnchor = prevSelection.anchor
  if nextAnchor.key != prevAnchor.key || nextAnchor.type != .text {
    return .other
  }

  let nextAnchorOffset = nextAnchor.offset
  let prevAnchorOffset = prevAnchor.offset
  let textDiff = nextText.lengthAsNSString() - prevText.lengthAsNSString()
  if textDiff == 1 && prevAnchorOffset == nextAnchorOffset - 1 {
    return .insertCharacterAfterSelection
  }
  if textDiff == -1 && prevAnchorOffset == nextAnchorOffset + 1 {
    return .deleteCharacterBeforeSelection
  }
  if textDiff == -1 && prevAnchorOffset == nextAnchorOffset {
    return .deleteCharacterAfterSelection
  }
  return .other
}

public func createEmptyHistoryState() -> HistoryState {
  HistoryState(current: nil, redoStack: [], undoStack: [])
}

func clearHistory(historyState: HistoryState) {
  historyState.undoStack.removeAll()
  historyState.redoStack.removeAll()
  historyState.current = nil
}


/*
 * 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.
 */

import Foundation

/*
 * This file, Mutations, contains the logic for Lexical's `non-controlled` mode. In this mode,
 * the UITextView directly updates its text storage with newly typed characters, and then we
 * read them back and update the Lexical data model afterwards.
 */

var isProcessingMutations = false

// called when some text has already been changed in non-controlled mode. Note that in this
// state, the Range Cache is not in sync with the Attributed String. (The Range Cache _is_ in
// sync with the EditorState.)
// This method should only be called if we've already decided that it's OK to operate in
// non-controlled mode, i.e. the mutation doesn't delete across node boundaries or insert
// somewhere that is without a TextNode.
internal func handleTextMutation(textStorage: TextStorage, rangeOfChange: NSRange, lengthDelta: Int) throws {
  guard let editor = getActiveEditor()
  else {
    throw LexicalError.invariantViolation("Failed to find editor")
  }
  editor.log(.other, .verbose)

  guard let point = try pointAtStringLocation(rangeOfChange.location, searchDirection: .forward, rangeCache: editor.rangeCache) else {
    editor.log(.other, .verbose, "Failed to find node")
    throw LexicalError.invariantViolation("Failed to find node")
  }

  let nodeKey = point.key
  guard let editorState = getActiveEditorState(),
        let rangeCacheItem = editor.rangeCache[nodeKey],
        let node = getNodeByKey(key: nodeKey) as? TextNode,
        editor.dirtyNodes.count == 0
  else {
    editor.log(.other, .verbose, "Failed to find node 2")
    throw LexicalError.invariantViolation("Failed to find node")
  }

  editor.log(.other, .verbose, "Before setting text: dirty nodes count \(editor.dirtyNodes.count)")

  let oldRange = rangeCacheItem.textRange()
  let newRange = NSRange(location: oldRange.location, length: oldRange.length + lengthDelta)

  if newRange.location + newRange.length <= textStorage.length {
    let text = textStorage.attributedSubstring(from: newRange).string

    // since we're updating from text view content, we also have to modify the range cache
    // without running the reconciler.
    try node.setText(text)
  } else {
    throw LexicalError.invariantViolation("mutation out of bounds: possible incorrect usage of non-controlled mode")
  }

  editor.log(.other, .verbose, "Before updating range cache: dirty nodes count \(editor.dirtyNodes.count)")

  updateRangeCacheForTextChange(nodeKey: nodeKey, delta: lengthDelta)

  editor.log(.other, .verbose, "After updating range cache: dirty nodes count \(editor.dirtyNodes.count)")

  // ensure correct attributes are in the UITextView
  // (Needed because UITextView doesn't propagate our custom attributes when inserting text)
  let styles = AttributeUtils.attributedStringStyles(from: node, state: editorState, theme: editor.getTheme())
  textStorage.setAttributes(styles, range: newRange)

  editor.log(.other, .verbose, "End of method: dirty nodes count \(editor.dirtyNodes.count)")
}

public func getIsProcessingMutations() -> Bool {
  return isProcessingMutations
}


/*
 * 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.
 */

import Foundation
import UIKit

public enum NodePart {
  case preamble
  case text
  case postamble
}

private struct ReconcilerInsertion {
  var location: Int
  var nodeKey: NodeKey
  var part: NodePart
}

private class ReconcilerState {
  internal init(
    currentEditorState: EditorState,
    pendingEditorState: EditorState,
    rangeCache: [NodeKey: RangeCacheItem],
    dirtyNodes: DirtyNodeMap,
    treatAllNodesAsDirty: Bool,
    markedTextOperation: MarkedTextOperation?
  ) {
    self.prevEditorState = currentEditorState
    self.nextEditorState = pendingEditorState
    self.prevRangeCache = rangeCache
    self.nextRangeCache = rangeCache // Use the previous range cache as a starting point
    self.locationCursor = 0
    self.rangesToDelete = []
    self.rangesToAdd = []
    self.dirtyNodes = dirtyNodes
    self.treatAllNodesAsDirty = treatAllNodesAsDirty
    self.markedTextOperation = markedTextOperation
    self.possibleDecoratorsToRemove = []
    self.decoratorsToAdd = []
    self.decoratorsToDecorate = []
  }

  let prevEditorState: EditorState
  let nextEditorState: EditorState
  let prevRangeCache: [NodeKey: RangeCacheItem]
  var nextRangeCache: [NodeKey: RangeCacheItem]
  var locationCursor: Int = 0
  var rangesToDelete: [NSRange]
  var rangesToAdd: [ReconcilerInsertion]
  let dirtyNodes: DirtyNodeMap
  let treatAllNodesAsDirty: Bool
  let markedTextOperation: MarkedTextOperation?
  var possibleDecoratorsToRemove: [NodeKey]
  var decoratorsToAdd: [NodeKey]
  var decoratorsToDecorate: [NodeKey]
}

/* Marked text is a difficult operation because it depends on us being in sync with some private state that
 * is held by the iOS keyboard. We can't set or read that state, so we have to make sure we do the things
 * that the keyboard is expecting.
 */
internal struct MarkedTextOperation {
  internal init(createMarkedText: Bool, selectionRangeToReplace: NSRange, markedTextString: String, markedTextInternalSelection: NSRange) {
    self.createMarkedText = createMarkedText
    self.selectionRangeToReplace = selectionRangeToReplace
    self.markedTextString = markedTextString
    self.markedTextInternalSelection = markedTextInternalSelection
  }

  let createMarkedText: Bool
  let selectionRangeToReplace: NSRange
  let markedTextString: String
  let markedTextInternalSelection: NSRange
}

internal enum Reconciler {

  internal static func updateEditorState(
    currentEditorState: EditorState,
    pendingEditorState: EditorState,
    editor: Editor,
    shouldReconcileSelection: Bool, // the situations where we would want to not do this include handling non-controlled mode
    markedTextOperation: MarkedTextOperation?
  ) throws {
    editor.log(.reconciler, .verbose)

    guard let textStorage = editor.textStorage else {
      fatalError("Cannot run reconciler on an editor with no text storage")
    }

    if let markedTextOperation, markedTextOperation.createMarkedText {
      guard shouldReconcileSelection == false else {
        editor.log(.reconciler, .warning, "should not reconcile selection whilst starting marked text!")
        throw LexicalError.invariantViolation("should not reconcile selection whilst starting marked text!")
      }
    }

    let currentSelection = currentEditorState.selection
    let nextSelection = pendingEditorState.selection
    let needsUpdate = editor.dirtyType != .noDirtyNodes

    let reconcilerState = ReconcilerState(currentEditorState: currentEditorState,
                                          pendingEditorState: pendingEditorState,
                                          rangeCache: editor.rangeCache,
                                          dirtyNodes: editor.dirtyNodes,
                                          treatAllNodesAsDirty: editor.dirtyType == .fullReconcile,
                                          markedTextOperation: markedTextOperation)

    try reconcileNode(key: kRootNodeKey, reconcilerState: reconcilerState)

    let previousMode = textStorage.mode
    textStorage.mode = .controllerMode
    textStorage.beginEditing()

    editor.log(.reconciler, .verbose, "about to do rangesToDelete: total \(reconcilerState.rangesToDelete.count)")
    var nonEmptyDeletionsCount = 0

    for deletionRange in reconcilerState.rangesToDelete.reversed() {
      if deletionRange.length > 0 {
        nonEmptyDeletionsCount += 1
        editor.log(.reconciler, .verboseIncludingUserContent, "deleting range \(NSStringFromRange(deletionRange)) `\((textStorage.string as NSString).substring(with: deletionRange))`")
        textStorage.deleteCharacters(in: deletionRange)
      }
    }

    editor.log(.reconciler, .verbose, "did rangesToDelete: non-empty \(nonEmptyDeletionsCount)")

    var markedTextAttributedString: NSAttributedString?
    var markedTextPointForAddition: Point?

    if let markedTextOperation {
      // Find the Point corresponding to the location where marked text will be added
      markedTextPointForAddition = try? pointAtStringLocation(
        markedTextOperation.selectionRangeToReplace.location,
        searchDirection: .forward,
        rangeCache: reconcilerState.nextRangeCache
      )
    }

    // Handle the decorators
    let decoratorsToRemove = reconcilerState.possibleDecoratorsToRemove.filter { key in
      return !reconcilerState.decoratorsToAdd.contains(key)
    }
    let decoratorsToDecorate = reconcilerState.decoratorsToDecorate.filter { key in
      return !reconcilerState.decoratorsToAdd.contains(key)
    }
    decoratorsToRemove.forEach { key in
      decoratorView(forKey: key, createIfNecessary: false)?.removeFromSuperview()
      destroyCachedDecoratorView(forKey: key)
      textStorage.decoratorPositionCache[key] = nil
    }
    reconcilerState.decoratorsToAdd.forEach { key in
      if editor.decoratorCache[key] == nil {
        editor.decoratorCache[key] = DecoratorCacheItem.needsCreation
      }
      guard let rangeCacheItem = reconcilerState.nextRangeCache[key] else { return }
      textStorage.decoratorPositionCache[key] = rangeCacheItem.location
    }
    decoratorsToDecorate.forEach { key in
      if let cacheItem = editor.decoratorCache[key], let view = cacheItem.view {
        editor.decoratorCache[key] = DecoratorCacheItem.needsDecorating(view)
      }
      guard let rangeCacheItem = reconcilerState.nextRangeCache[key] else { return }
      textStorage.decoratorPositionCache[key] = rangeCacheItem.location
    }

    editor.log(.reconciler, .verbose, "about to do rangesToAdd: total \(reconcilerState.rangesToAdd.count)")

    var nonEmptyRangesToAddCount = 0
    var rangesInserted: [NSRange] = []
    for insertion in reconcilerState.rangesToAdd {
      let attributedString = attributedStringFromInsertion(
        insertion,
        state: reconcilerState.nextEditorState,
        theme: editor.getTheme())
      if attributedString.length > 0 {
        nonEmptyRangesToAddCount += 1
        editor.log(.reconciler, .verboseIncludingUserContent, "inserting at \(insertion.location), `\(attributedString.string)`")
        textStorage.insert(attributedString, at: insertion.location)
        rangesInserted.append(NSRange(location: insertion.location, length: attributedString.length))

        // If this insertion corresponds to the marked text location, keep hold of the attributed string.
        if let pointForAddition = markedTextPointForAddition, let length = markedTextOperation?.markedTextString.lengthAsNSString() {
          if insertion.part == .text && pointForAddition.key == insertion.nodeKey && pointForAddition.offset + length <= attributedString.length {
            markedTextAttributedString = attributedString
          }
        }
      }
    }

    // Fix up all attributes afterwards. Doing the fix during the insertion loop above will cause incorrect normalisation of NSParagraphStyles.
    for range in rangesInserted {
      textStorage.fixAttributes(in: range)
    }

    editor.log(.reconciler, .verbose, "did rangesToAdd: non-empty \(nonEmptyRangesToAddCount)")

    // BLOCK LEVEL ATTRIBUTES

    let lastDescendentAttributes = getRoot()?.getLastChild()?.getAttributedStringAttributes(theme: editor.getTheme())

    // TODO: this iteration applies the attributes in an arbitrary order. If we are to handle nesting nodes with these block level attributes
    // we may want to apply them in a deterministic order, and also make them nest additively (i.e. for when two blocks start at the same paragraph)
    var nodesToApplyBlockAttributes: Set<NodeKey> = []
    if reconcilerState.treatAllNodesAsDirty {
      nodesToApplyBlockAttributes = Set(pendingEditorState.nodeMap.keys)
    } else {
      for nodeKey in reconcilerState.dirtyNodes.keys {
        guard let node = getNodeByKey(key: nodeKey) else { continue }
        nodesToApplyBlockAttributes.insert(nodeKey)
        for parentNodeKey in node.getParentKeys() {
          nodesToApplyBlockAttributes.insert(parentNodeKey)
        }
      }
    }
    let rangeCache = reconcilerState.nextRangeCache
    for nodeKey in nodesToApplyBlockAttributes {
      guard let node = getNodeByKey(key: nodeKey),
            node.isAttached(),
            let cacheItem = rangeCache[nodeKey],
            let attributes = node.getBlockLevelAttributes(theme: editor.getTheme())
      else { continue }

      AttributeUtils.applyBlockLevelAttributes(attributes, cacheItem: cacheItem, textStorage: textStorage, nodeKey: nodeKey, lastDescendentAttributes: lastDescendentAttributes ?? [:])
    }

    editor.rangeCache = reconcilerState.nextRangeCache
    textStorage.endEditing()
    textStorage.mode = previousMode

    if let markedTextOperation,
       markedTextOperation.createMarkedText,
       let markedTextAttributedString,
       let startPoint = markedTextPointForAddition,
       let frontend = editor.frontend {
      // We have a marked text operation, an attributed string, we know the Point at which it should be added.
      // Note that the text has _already_ been inserted into the TextStorage, so we actually have to _replace_ the
      // marked text range with the same text, but via a marked text operation. Hence we deduce the end point
      // of the marked text, set a fake selection using it, and then tell the text view to go ahead and start a
      // marked text operation.
      let length = markedTextOperation.markedTextString.lengthAsNSString()
      let endPoint = Point(key: startPoint.key, offset: startPoint.offset + length, type: .text)
      try frontend.updateNativeSelection(from: RangeSelection(anchor: startPoint, focus: endPoint, format: TextFormat()))
      let attributedSubstring = markedTextAttributedString.attributedSubstring(from: NSRange(location: startPoint.offset, length: length))
      editor.frontend?.setMarkedTextFromReconciler(attributedSubstring, selectedRange: markedTextOperation.markedTextInternalSelection)

      // do not do selection reconcile after marked text!
      // The selection will be correctly set as part of the setMarkedTextFromReconciler() call.
      return
    }

    if shouldReconcileSelection && (needsUpdate || nextSelection == nil) {
      try reconcileSelection(prevSelection: currentSelection, nextSelection: nextSelection, editor: editor)
    }
  }

  private static func reconcileNode(key: NodeKey, reconcilerState: ReconcilerState) throws {
    guard let prevNode = reconcilerState.prevEditorState.nodeMap[key], let nextNode = reconcilerState.nextEditorState.nodeMap[key] else {
      throw LexicalError.invariantViolation(
        "reconcileNode should only be called when a node is present in both node maps, otherwise create or delete should be called")
    }
    guard let prevRange = reconcilerState.prevRangeCache[key] else {
      throw LexicalError.invariantViolation(
        "Node map entry for '\(key)' not found")
    }

    let isDirty = reconcilerState.dirtyNodes[key] != nil || reconcilerState.treatAllNodesAsDirty

    if prevNode === nextNode && !isDirty {
      if prevRange.location != reconcilerState.locationCursor {
        // we only have to update the location of this and children; all other cache values are valid
        // NB, the updateLocationOfNonDirtyNode method handles updating the reconciler state location cursor
        updateLocationOfNonDirtyNode(key: key, reconcilerState: reconcilerState)
      } else {
        // cache is already valid, just update the cursor
        // no need to iterate into children, since their cache values are valid too and we've got a cached childrenLength we can use.
        reconcilerState.locationCursor += prevRange.preambleLength + prevRange.textLength + prevRange.childrenLength + prevRange.postambleLength
      }
      return
    }

    var nextRangeCacheItem = RangeCacheItem()
    nextRangeCacheItem.location = reconcilerState.locationCursor

    let nextPreambleLength = nextNode.getPreamble().lengthAsNSString()
    createAddRemoveRanges(
      key: key,
      prevLocation: prevRange.location,
      prevLength: prevRange.preambleLength,
      nextLength: nextPreambleLength,
      reconcilerState: reconcilerState,
      part: .preamble
    )
    nextRangeCacheItem.preambleLength = nextPreambleLength

    // right, now we have finished the preamble, and the cursor is in the right place. Time for children.
    if nextNode is ElementNode {
      let cursorBeforeChildren = reconcilerState.locationCursor
      try reconcileChildren(key: key, reconcilerState: reconcilerState)
      nextRangeCacheItem.childrenLength = reconcilerState.locationCursor - cursorBeforeChildren
    } else if nextNode is DecoratorNode {
      reconcilerState.decoratorsToDecorate.append(key)
    }

    let nextTextLength = nextNode.getTextPart().lengthAsNSString()
    createAddRemoveRanges(
      key: key,
      prevLocation: prevRange.location + prevRange.preambleLength + prevRange.childrenLength,
      prevLength: prevRange.textLength,
      nextLength: nextTextLength,
      reconcilerState: reconcilerState,
      part: .text
    )
    nextRangeCacheItem.textLength = nextTextLength

    let nextPostambleLength = nextNode.getPostamble().lengthAsNSString()
    createAddRemoveRanges(
      key: key,
      prevLocation: prevRange.location + prevRange.preambleLength + prevRange.childrenLength + prevRange.textLength,
      prevLength: prevRange.postambleLength,
      nextLength: nextPostambleLength,
      reconcilerState: reconcilerState,
      part: .postamble
    )
    nextRangeCacheItem.postambleLength = nextPostambleLength

    reconcilerState.nextRangeCache[key] = nextRangeCacheItem
  }

  private static func createAddRemoveRanges(
    key: NodeKey,
    prevLocation: Int,
    prevLength: Int,
    nextLength: Int,
    reconcilerState: ReconcilerState,
    part: NodePart
  ) {
    if prevLength > 0 {
      let prevRange = NSRange(location: prevLocation, length: prevLength)
      reconcilerState.rangesToDelete.append(prevRange)
    }
    if nextLength > 0 {
      let insertion = ReconcilerInsertion(location: reconcilerState.locationCursor, nodeKey: key, part: part)
      reconcilerState.rangesToAdd.append(insertion)
    }
    reconcilerState.locationCursor += nextLength
  }

  private static func createNode(key: NodeKey, reconcilerState: ReconcilerState) {
    guard let nextNode = reconcilerState.nextEditorState.nodeMap[key] else {
      return
    }

    var nextRangeCacheItem = RangeCacheItem()
    nextRangeCacheItem.location = reconcilerState.locationCursor

    let nextPreambleLength = nextNode.getPreamble().lengthAsNSString()
    let preambleInsertion = ReconcilerInsertion(location: reconcilerState.locationCursor, nodeKey: key, part: .preamble)
    reconcilerState.rangesToAdd.append(preambleInsertion)
    reconcilerState.locationCursor += nextPreambleLength
    nextRangeCacheItem.preambleLength = nextPreambleLength

    if let nextNode = nextNode as? ElementNode, nextNode.children.count > 0 {
      let cursorBeforeChildren = reconcilerState.locationCursor
      createChildren(nextNode.children, range: 0...nextNode.children.count - 1, reconcilerState: reconcilerState)
      nextRangeCacheItem.childrenLength = reconcilerState.locationCursor - cursorBeforeChildren
    } else if nextNode is DecoratorNode {
      reconcilerState.decoratorsToAdd.append(key)
    }

    let nextTextLength = nextNode.getTextPart().lengthAsNSString()
    let textInsertion = ReconcilerInsertion(location: reconcilerState.locationCursor, nodeKey: key, part: .text)
    reconcilerState.rangesToAdd.append(textInsertion)
    reconcilerState.locationCursor += nextTextLength
    nextRangeCacheItem.textLength = nextTextLength

    let nextPostambleLength = nextNode.getPostamble().lengthAsNSString()
    let postambleInsertion = ReconcilerInsertion(location: reconcilerState.locationCursor, nodeKey: key, part: .postamble)
    reconcilerState.rangesToAdd.append(postambleInsertion)
    reconcilerState.locationCursor += nextPostambleLength
    nextRangeCacheItem.postambleLength = nextPostambleLength

    reconcilerState.nextRangeCache[key] = nextRangeCacheItem
  }

  private static func destroyNode(key: NodeKey, reconcilerState: ReconcilerState) {
    guard let prevNode = reconcilerState.prevEditorState.nodeMap[key], let prevRangeCacheItem = reconcilerState.prevRangeCache[key] else {
      return
    }

    let prevPreambleRange = NSRange(location: prevRangeCacheItem.location, length: prevRangeCacheItem.preambleLength)
    reconcilerState.rangesToDelete.append(prevPreambleRange)

    if let prevNode = prevNode as? ElementNode, prevNode.children.count > 0 {
      destroyChildren(prevNode.children, range: 0...prevNode.children.count - 1, reconcilerState: reconcilerState)
    } else if prevNode is DecoratorNode {
      reconcilerState.possibleDecoratorsToRemove.append(key)
    }

    let prevTextRange = NSRange(location: prevRangeCacheItem.location + prevRangeCacheItem.preambleLength + prevRangeCacheItem.childrenLength, length: prevRangeCacheItem.textLength)
    reconcilerState.rangesToDelete.append(prevTextRange)

    let prevPostambleRange = NSRange(location: prevRangeCacheItem.location + prevRangeCacheItem.preambleLength + prevRangeCacheItem.childrenLength + prevRangeCacheItem.textLength, length: prevRangeCacheItem.postambleLength)
    reconcilerState.rangesToDelete.append(prevPostambleRange)

    if reconcilerState.nextEditorState.nodeMap[key] == nil {
      reconcilerState.nextRangeCache.removeValue(forKey: key)
    }
  }

  private static func reconcileChildren(key: NodeKey, reconcilerState: ReconcilerState) throws {
    guard let prevNode = reconcilerState.prevEditorState.nodeMap[key] as? ElementNode,
          let nextNode = reconcilerState.nextEditorState.nodeMap[key] as? ElementNode else {
      return
    }
    // in JS, this method does a few optimisation codepaths, then calls to the slow path reconcileNodeChildren. I'll not program the optimisations yet.
    try reconcileNodeChildren(
      prevChildren: prevNode.children,
      nextChildren: nextNode.children,
      prevChildrenLength: prevNode.children.count,
      nextChildrenLength: nextNode.children.count,
      reconcilerState: reconcilerState)
  }

  private static func reconcileNodeChildren(prevChildren: [NodeKey],
                                            nextChildren: [NodeKey],
                                            prevChildrenLength: Int,
                                            nextChildrenLength: Int,
                                            reconcilerState: ReconcilerState) throws {
    let prevEndIndex = prevChildrenLength - 1
    let nextEndIndex = nextChildrenLength - 1
    var prevIndex = 0
    var nextIndex = 0

    // the sets exist as an optimisation for performance reasons
    var prevChildrenSet: Set<NodeKey>?
    var nextChildrenSet: Set<NodeKey>?

    while prevIndex <= prevEndIndex && nextIndex <= nextEndIndex {
      let prevKey = prevChildren[prevIndex]
      let nextKey = nextChildren[nextIndex]

      if prevKey == nextKey {
        try reconcileNode(key: nextKey, reconcilerState: reconcilerState)
        prevIndex += 1
        nextIndex += 1
      } else {
        if prevChildrenSet == nil {
          prevChildrenSet = Set(prevChildren)
        }
        if nextChildrenSet == nil {
          nextChildrenSet = Set(nextChildren)
        }

        let nextHasPrevKey = nextChildren.contains(prevKey)
        let prevHasNextKey = prevChildren.contains(nextKey)

        if !nextHasPrevKey {
          // Remove prev
          destroyNode(key: prevKey, reconcilerState: reconcilerState)
          prevIndex += 1
        } else if !prevHasNextKey {
          // Create next
          createNode(key: nextKey, reconcilerState: reconcilerState)
          nextIndex += 1
        } else {
          // Move next -- destroy old and then insert new. (The counterpart will occur later in the loop!)
          destroyNode(key: prevKey, reconcilerState: reconcilerState)
          createNode(key: nextKey, reconcilerState: reconcilerState)
          prevIndex += 1
          nextIndex += 1
        }
      }
    }

    let appendNewChildren = prevIndex > prevEndIndex
    let removeOldChildren = nextIndex > nextEndIndex

    if appendNewChildren && !removeOldChildren {
      createChildren(nextChildren, range: nextIndex...nextEndIndex, reconcilerState: reconcilerState)
    } else if removeOldChildren && !appendNewChildren {
      destroyChildren(prevChildren, range: prevIndex...prevEndIndex, reconcilerState: reconcilerState)
    }
  }

  private static func createChildren(_ children: [NodeKey], range: ClosedRange<Int>, reconcilerState: ReconcilerState) {
    for child in children[range] {
      createNode(key: child, reconcilerState: reconcilerState)
    }
  }

  private static func destroyChildren(_ children: [NodeKey], range: ClosedRange<Int>, reconcilerState: ReconcilerState) {
    for child in children[range] {
      destroyNode(key: child, reconcilerState: reconcilerState)
    }
  }

  private static func updateLocationOfNonDirtyNode(key: NodeKey, reconcilerState: ReconcilerState) {
    // not a typo that I'm setting nextRangeCacheItem to prevRangeCache[key]. We want to start with the prev cache item and update it.
    guard var nextRangeCacheItem = reconcilerState.prevRangeCache[key], let nextNode = reconcilerState.nextEditorState.nodeMap[key] else {
      // expected range cache entry to already exist
      return
    }
    nextRangeCacheItem.location = reconcilerState.locationCursor
    reconcilerState.nextRangeCache[key] = nextRangeCacheItem

    reconcilerState.locationCursor += nextRangeCacheItem.preambleLength
    if let nextNode = nextNode as? ElementNode {
      for childNodeKey in nextNode.children {
        updateLocationOfNonDirtyNode(key: childNodeKey, reconcilerState: reconcilerState)
      }
    }

    reconcilerState.locationCursor += nextRangeCacheItem.textLength
    reconcilerState.locationCursor += nextRangeCacheItem.postambleLength
    return
  }

  private static func attributedStringFromInsertion(
    _ insertion: ReconcilerInsertion,
    state: EditorState,
    theme: Theme
  ) -> NSAttributedString {
    guard let node = state.nodeMap[insertion.nodeKey] else {
      return NSAttributedString()
    }

    var attributedString: NSAttributedString

    switch insertion.part {
    case .text:
      attributedString = NSAttributedString(string: node.getTextPart())
    case .preamble:
      attributedString = NSAttributedString(string: node.getPreamble())
    case .postamble:
      attributedString = NSAttributedString(string: node.getPostamble())
    }

    attributedString = AttributeUtils.attributedStringByAddingStyles(
      attributedString,
      from: node,
      state: state,
      theme: theme)

    return attributedString
  }

  private static func reconcileSelection(
    prevSelection: BaseSelection?,
    nextSelection: BaseSelection?,
    editor: Editor) throws {
    guard let nextSelection else {
      if let prevSelection {
        if !prevSelection.dirty {
          return
        }

        editor.frontend?.resetSelectedRange()
      }

      return
    }

    // TODO: if node selection, go tell decorator nodes to select themselves!

    try editor.frontend?.updateNativeSelection(from: nextSelection)
  }
}

internal func performReconcilerSanityCheck(
  editor sanityCheckEditor: Editor,
  expectedOutput: NSAttributedString) throws {
  // TODO @amyworrall: this was commented out during the Frontend refactor. Create a new Frontend that contains
  // a TextKit stack but no selection or UI. Use that to re-implement the reconciler.

  //    // create new editor to reconcile within
  //    let editor = Editor(
  //      featureFlags: FeatureFlags(reconcilerSanityCheck: false),
  //      editorConfig: EditorConfig(theme: sanityCheckEditor.getTheme(), plugins: []))
  //    editor.textStorage = TextStorage()
  //
  //    try editor.setEditorState(sanityCheckEditor.getEditorState())
  //
  //    if let textStorage = editor.textStorage, !expectedOutput.isEqual(to: textStorage) {
  //      throw LexicalError.sanityCheck(
  //        errorMessage: "Failed sanity check",
  //        textViewText: expectedOutput.string,
  //        fullReconcileText: textStorage.string)
  //    }
}


/*
 * 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.
 */

import Foundation

// MARK: - Datatypes
enum PartialCodingKeys: String, CodingKey {
  case type
}

public struct SerializedEditorState: Codable {
  enum RootCodingKeys: String, CodingKey {
    case root
  }

  public var rootNode: RootNode?

  public init(rootNode: RootNode) {
    self.rootNode = rootNode
  }

  public init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: RootCodingKeys.self)
    self.rootNode = try container.decode(RootNode.self, forKey: .root)
  }

  public func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: RootCodingKeys.self)
    try container.encode(rootNode, forKey: .root)
  }
}

public struct SerializedNodeArray: Decodable {
  enum PartialCodingKeys: String, CodingKey {
    case type
  }

  public var nodeArray: [Node]

  public init(nodeArray: [Node]) {
    self.nodeArray = nodeArray
  }

  public init(from decoder: Decoder) throws {
    var container = try decoder.unkeyedContainer()
    var nodeArray = [Node]()

    self.nodeArray = nodeArray

    guard let editor = getActiveEditor() else { return }

    let deserializationMap = editor.registeredNodes

    while !container.isAtEnd {
      var containerCopy = container
      let unprocessedContainer = try container.nestedContainer(keyedBy: PartialCodingKeys.self)
      let type = try NodeType(rawValue: unprocessedContainer.decode(String.self, forKey: .type))
      let klass = deserializationMap[type] ?? UnknownNode.self

      do {
        let decoder = try containerCopy.superDecoder()
        let decodedNode = try klass.init(from: decoder)
        nodeArray.append(decodedNode)
      } catch {
        print(error)
      }
    }

    self.nodeArray = nodeArray
  }
}

public typealias DeserializationConstructor = (Decoder) throws -> Node
typealias DeserializationMapping = [NodeType: DeserializationConstructor]

// MARK: - Utilities

// NB: We are assuming JSON serialization here initially
let sharedDecoder = JSONDecoder()

let defaultDeserializationMapping: DeserializationMapping = [
  NodeType.root: { decoder in try RootNode(from: decoder) },
  NodeType.text: { decoder in try TextNode(from: decoder) },
  NodeType.element: { decoder in try ElementNode(from: decoder) },
  NodeType.heading: { decoder in try HeadingNode(from: decoder) },
  NodeType.paragraph: { decoder in try ParagraphNode(from: decoder) },
  NodeType.quote: { decoder in try QuoteNode(from: decoder) }
]

func makeDeserializationMap() -> DeserializationMapping {
  return defaultDeserializationMapping
}


/*
 * 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.
 */

import Foundation

public func updateTextFormat(type: TextFormatType, editor: Editor) throws {
  guard getActiveEditor() != nil else {
    throw LexicalError.invariantViolation("Must have editor")
  }
  guard let selection = try getSelection() as? RangeSelection else {
    return
  }

  try selection.formatText(formatType: type)
}