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

@testable import Lexical
import XCTest

class SelectionUtilsTests: XCTestCase {
  func testCreatePoint() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      let textNode = TextNode()
      try textNode.setText("hello ")

      let textNode2 = TextNode()
      try textNode2.setText("world")

      let paragraphNode = ParagraphNode()
      try paragraphNode.append([textNode])
      try paragraphNode.append([textNode2])

      let startPoint = createPoint(key: textNode.key, offset: 0, type: .text)
      let endPoint = createPoint(key: textNode2.key, offset: 4, type: .text)
      let anotherPoint = createPoint(key: paragraphNode.key, offset: 2, type: .element)
      let selection = RangeSelection(anchor: startPoint, focus: endPoint, format: TextFormat())
      let selection2 = RangeSelection(anchor: anotherPoint, focus: anotherPoint, format: TextFormat())

      XCTAssertEqual(startPoint.offset, 0)
      XCTAssertEqual(anotherPoint.type, .element)

      XCTAssertEqual(try startPoint.getNode().key, textNode.key)
      XCTAssertEqual(try endPoint.getNode().key, textNode2.key)

      XCTAssertTrue(try startPoint.getNode() === textNode, "startPoint's node should be same as textNode")
      XCTAssertTrue(try endPoint.getNode() === textNode2, "endPoint's node should be same as textNode2")
      XCTAssertFalse(selection == selection2, "both objects point to different selection")

      XCTAssertFalse(try startPoint.isAtNodeEnd(), "startPoint is at start at textNode")
      XCTAssertFalse(try endPoint.isAtNodeEnd(), "endPoint doesn't point to last node")
      XCTAssertTrue(try anotherPoint.isAtNodeEnd(), "anotherPoint includes both children of paragraphNode")
    }
  }

  func testSelectPointOnNode() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      let textNode = TextNode()
      try textNode.setText("hello ")

      let textNode2 = TextNode()
      try textNode2.setText("world")

      let textNode3 = TextNode()
      try textNode3.setText("again!")

      let paragraphNode = ParagraphNode()
      try paragraphNode.append([textNode])
      try paragraphNode.append([textNode2])
      try paragraphNode.append([textNode3])

      let startPoint = createPoint(key: textNode.key, offset: 0, type: .text)
      let endPoint = createPoint(key: textNode2.key, offset: 4, type: .text)

      selectPointOnNode(point: startPoint, node: textNode2)
      selectPointOnNode(point: endPoint, node: textNode3)

      XCTAssertEqual(startPoint.key, textNode2.key)
      XCTAssertEqual(endPoint.key, textNode3.key)
    }
  }

  func testCreateNativeSelectionInSameNode() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      createExampleNodeTree()
    }

    try editor.getEditorState().read {
      let selection = RangeSelection(anchor: Point(key: "2", offset: 1, type: .text),
                                     focus: Point(key: "2", offset: 3, type: .text),
                                     format: TextFormat())

      let textSelection = try createNativeSelection(from: selection, editor: editor)
      XCTAssertNotNil(textSelection.range)
      XCTAssertEqual(textSelection.affinity, .forward)
      XCTAssertEqual(textSelection.range?.location, 7)
      XCTAssertEqual(textSelection.range?.length, 2)

      let selection2 = RangeSelection(anchor: Point(key: "2", offset: 6, type: .text),
                                      focus: Point(key: "2", offset: 2, type: .text),
                                      format: TextFormat())

      let textSelection2 = try createNativeSelection(from: selection2, editor: editor)
      XCTAssertNotNil(textSelection2.range)
      XCTAssertEqual(textSelection2.affinity, .backward)
      XCTAssertEqual(textSelection2.range?.location, 8)
      XCTAssertEqual(textSelection2.range?.length, 4)

      let selection3 = RangeSelection(anchor: Point(key: "6", offset: 6, type: .text),
                                      focus: Point(key: "6", offset: 6, type: .text),
                                      format: TextFormat())

      let textSelection3 = try createNativeSelection(from: selection3, editor: editor)
      XCTAssertNotNil(textSelection3.range)
      XCTAssertEqual(textSelection3.affinity, .forward)
      XCTAssertEqual(textSelection3.range?.location, 58)
      XCTAssertEqual(textSelection3.range?.length, 0)
    }
  }

  func testCreateNativeSelectionBetweenSiblings() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      createExampleNodeTree()
    }

    try editor.getEditorState().read {
      let selection = RangeSelection(anchor: Point(key: "1", offset: 1, type: .text),
                                     focus: Point(key: "2", offset: 3, type: .text),
                                     format: TextFormat())

      let textSelection = try createNativeSelection(from: selection, editor: editor)
      XCTAssertNotNil(textSelection.range)
      XCTAssertEqual(textSelection.affinity, .forward)
      XCTAssertEqual(textSelection.range?.location, 1)
      XCTAssertEqual(textSelection.range?.length, 8)

      let selection2 = RangeSelection(anchor: Point(key: "2", offset: 4, type: .text),
                                      focus: Point(key: "1", offset: 3, type: .text),
                                      format: TextFormat())

      let textSelection2 = try createNativeSelection(from: selection2, editor: editor)
      XCTAssertNotNil(textSelection2.range)
      XCTAssertEqual(textSelection2.affinity, .backward)
      XCTAssertEqual(textSelection2.range?.location, 3)
      XCTAssertEqual(textSelection2.range?.length, 7)
    }
  }

  func testCreateNativeSelectionWithinElementNode() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      guard let editorState = getActiveEditorState(),
            let rootNode = editorState.getRootNode()
      else {
        XCTFail("should have editor state")
        return
      }

      let textNode1 = TextNode() // key 1
      let textNode2 = TextNode() // key 2
      try textNode1.setText("Hello ")
      try textNode1.setBold(true)
      try textNode2.setText("Adios")

      guard let paragraphNode0 = rootNode.getFirstChild() as? ParagraphNode else {
        XCTFail()
        return
      }

      try paragraphNode0.append([textNode1, textNode2])

      let textNode3 = TextNode() // key 3
      let textNode4 = TextNode() // key 4
      let textNode5 = TextNode() // key 5
      try textNode3.setText("This is a new test.")
      try textNode3.setBold(true)
      try textNode4.setText("Checking selection between")
      try textNode5.setText("element types")
      try textNode5.setBold(true)

      let paragraphNode1 = ParagraphNode() // key 6

      try paragraphNode1.append([textNode3, textNode4, textNode5])

      try rootNode.append([paragraphNode1])
    }

    try editor.getEditorState().read {
      let selection = RangeSelection(anchor: Point(key: "0", offset: 1, type: .element),
                                     focus: Point(key: "0", offset: 1, type: .element),
                                     format: TextFormat())

      let textSelection = try createNativeSelection(from: selection, editor: editor)
      XCTAssertNotNil(textSelection.range)
      XCTAssertEqual(textSelection.affinity, .forward, "1 fail")
      XCTAssertEqual(textSelection.range?.location, 6, "Offset for native selection should be 6")
      XCTAssertEqual(textSelection.range?.length, 0, "length of current selection should be 0")

      let selection2 = RangeSelection(anchor: Point(key: "6", offset: 2, type: .element),
                                      focus: Point(key: "6", offset: 1, type: .element),
                                      format: TextFormat())

      let textSelection2 = try createNativeSelection(from: selection2, editor: editor)
      XCTAssertNotNil(textSelection2.range)
      XCTAssertEqual(textSelection2.affinity, .backward, "2 fail")
      XCTAssertEqual(textSelection2.range?.location, 31)
      XCTAssertEqual(textSelection2.range?.length, 26)

      let selection3 = RangeSelection(anchor: Point(key: "0", offset: 1, type: .element),
                                      focus: Point(key: "6", offset: 2, type: .element),
                                      format: TextFormat())

      let textSelection3 = try createNativeSelection(from: selection3, editor: editor)
      XCTAssertNotNil(textSelection3.range)
      XCTAssertEqual(textSelection3.affinity, .forward, "3 fail")
      XCTAssertEqual(textSelection3.range?.location, 6)
      XCTAssertEqual(textSelection3.range?.length, 51)
    }
  }

  func testExhaustiveLocationRoundtrip() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      createExampleNodeTree()
    }

    for i in 0...view.textStorage.string.lengthAsNSString() {
      try editor.update {
        guard let point = try pointAtStringLocation(i, searchDirection: .forward, rangeCache: editor.rangeCache) else { XCTFail("Couldn't generate point for string location"); return }
        guard let location = try stringLocationForPoint(point, editor: editor) else { XCTFail("Couldn't generate string location for point"); return }
        XCTAssertEqual(i, location, "Location did not match after a roundtrip")
      }
    }
  }

  func testExhaustiveSelectionRoundtrip() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      createExampleNodeTree()
    }

    for i in 0...view.textStorage.string.lengthAsNSString() {
      for j in i...view.textStorage.string.lengthAsNSString() {
        try editor.update {
          guard let point = try pointAtStringLocation(i, searchDirection: .forward, rangeCache: editor.rangeCache),
                let point2 = try pointAtStringLocation(j, searchDirection: .forward, rangeCache: editor.rangeCache)
          else { XCTFail("Couldn't generate points for string location"); return }

          guard let location1 = try stringLocationForPoint(point, editor: editor),
                let location2 = try stringLocationForPoint(point2, editor: editor)
          else { XCTFail("Couldn't generate string location for point"); return }

          XCTAssertEqual(i, location1, "Point for i does not match")
          XCTAssertEqual(j, location2, "Point for j does not match")

          if i != j {
            // in this algorithm, i will always be before or equal to j. If they're not equal, i should be before j.
            XCTAssertTrue(try point.isBefore(point: point2), "isBefore incorrect")
          }

          let selection = RangeSelection(anchor: point, focus: point2, format: TextFormat())
          let textViewSelection = try createNativeSelection(from: selection, editor: editor)
          guard let range = textViewSelection.range else {
            XCTFail("Couldn't generate text selection with range")
            return
          }
          XCTAssertEqual(i, range.location, "Location did not match after a roundtrip")
          XCTAssertEqual(j, range.location + range.length, "Length did not match after a roundtrip")
        }
      }
    }
  }

  func testCreateEmptyRangeSelection() {
    let selection = createEmptyRangeSelection()

    XCTAssertNotNil(selection)
    XCTAssertEqual(selection.anchor.key, kRootNodeKey)
    XCTAssertEqual(selection.anchor.offset, 0)
    XCTAssertEqual(selection.anchor.type, SelectionType.element)
    XCTAssertEqual(selection.focus.key, kRootNodeKey)
    XCTAssertEqual(selection.focus.offset, 0)
    XCTAssertEqual(selection.focus.type, SelectionType.element)
  }

  func testCreateRangeSelection() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      createExampleNodeTree()
    }

    view.textView.selectedRange = NSRange(location: 0, length: 5)

    try editor.update {
      guard let selection = editor.testing_getPendingEditorState()?.selection as? RangeSelection else {
        XCTFail("Expected range selection")
        return
      }
      XCTAssertEqual(selection.anchor.key, "1")
      XCTAssertEqual(selection.anchor.offset, 0)
      XCTAssertEqual(selection.focus.key, "1")
      XCTAssertEqual(selection.focus.offset, 5)
    }
  }

  func testadjustPointOffsetForMergedSibling() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      let textNode = TextNode()
      try textNode.setText("hello ")

      let textNode2 = TextNode()
      try textNode2.setText("world")

      let paragraphNode = ParagraphNode()
      try paragraphNode.append([textNode])
      try paragraphNode.append([textNode2])

      let startPoint = createPoint(key: textNode.key, offset: 0, type: .text)
      let endPoint = createPoint(key: textNode2.key, offset: 4, type: .text)

      adjustPointOffsetForMergedSibling(point: startPoint,
                                        isBefore: true,
                                        key: textNode.key,
                                        target: textNode,
                                        textLength: 6)

      adjustPointOffsetForMergedSibling(point: endPoint,
                                        isBefore: false,
                                        key: textNode2.key,
                                        target: textNode2,
                                        textLength: 5)

      XCTAssertEqual(startPoint.key, textNode.key)
      XCTAssertEqual(startPoint.offset, 0)
      XCTAssertEqual(endPoint.offset, 9)
    }
  }

  func testmoveSelectionPointToSibling() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      let textNode = TextNode()
      try textNode.setText("hello ")

      let paragraphNode = ParagraphNode()
      try paragraphNode.append([textNode])

      let startPoint = createPoint(key: textNode.key, offset: 1, type: .text)

      moveSelectionPointToSibling(point: startPoint, node: textNode, parent: paragraphNode)
      XCTAssertTrue(startPoint.offset == 0)
    }
  }

  func testUpdateElementSelectionOnCreateDeleteNode() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      let textNode = TextNode()
      try textNode.setText("hello")

      let textNode2 = TextNode()
      try textNode2.setText("world")

      let paragraphNode = ParagraphNode()
      try paragraphNode.append([textNode])
      try paragraphNode.append([textNode2])

      let startPoint = createPoint(key: textNode.key, offset: 3, type: .text)
      let endPoint = createPoint(key: textNode.key, offset: 3, type: .text)
      let selection = RangeSelection(anchor: startPoint, focus: endPoint, format: TextFormat())
      try updateElementSelectionOnCreateDeleteNode(selection: selection, parentNode: textNode, nodeOffset: 2, times: 1)
      XCTAssertEqual(selection.focus.offset, 4)

      let startPoint2 = createPoint(key: textNode.key, offset: 3, type: .text)
      let endPoint2 = createPoint(key: textNode2.key, offset: 5, type: .text)
      let selection2 = RangeSelection(anchor: startPoint2, focus: endPoint2, format: TextFormat())
      try updateElementSelectionOnCreateDeleteNode(selection: selection2, parentNode: textNode, nodeOffset: 2, times: 1)
      XCTAssertEqual(selection2.focus.offset, 5)

      let startPoint3 = createPoint(key: textNode.key, offset: 3, type: .text)
      let endPoint3 = createPoint(key: textNode2.key, offset: 5, type: .text)
      let selection3 = RangeSelection(anchor: startPoint3, focus: endPoint3, format: TextFormat())
      try updateElementSelectionOnCreateDeleteNode(selection: selection3, parentNode: textNode2, nodeOffset: 2, times: 1)
      XCTAssertEqual(selection3.focus.offset, 6)
    }
  }

  func testUpdateSelectionResolveTextNodes() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      guard let editorState = getActiveEditorState(),
            let rootNode = editorState.getRootNode()
      else {
        XCTFail("should have editor state")
        return
      }
      let textNode = TextNode()
      try textNode.setText("hello")

      let textNode2 = TextNode()
      try textNode2.setText("world")

      let paragraphNode = ParagraphNode()
      try paragraphNode.append([textNode])
      try paragraphNode.append([textNode2])

      let testNode1 = TextNode()
      let testNode2 = TextNode()
      let testNode3 = TextNode()
      try testNode1.setText("This is a new test.")
      try testNode2.setText("Checking selection between")
      try testNode3.setText("element types")

      let paragraphNode1 = ParagraphNode()

      try paragraphNode1.append([testNode1])
      try paragraphNode1.append([testNode2])
      try paragraphNode1.append([testNode3])

      try rootNode.append([paragraphNode])
      try rootNode.append([paragraphNode1])

      let startPoint = createPoint(key: textNode.key, offset: 3, type: .text)
      let endPoint = createPoint(key: textNode.key, offset: 3, type: .text)
      let selection = RangeSelection(anchor: startPoint, focus: endPoint, format: TextFormat())
      try updateSelectionResolveTextNodes(selection: selection)
      XCTAssertEqual(selection.focus.offset, 3)

      let startPoint1 = createPoint(key: textNode.key, offset: 3, type: .text)
      let endPoint1 = createPoint(key: paragraphNode.key, offset: 4, type: .element)
      let selection1 = RangeSelection(anchor: startPoint1, focus: endPoint1, format: TextFormat())
      try updateSelectionResolveTextNodes(selection: selection1)
      XCTAssertEqual(selection1.focus.offset, 5)

      let startPoint2 = createPoint(key: paragraphNode.key, offset: 3, type: .element)
      let endPoint2 = createPoint(key: textNode2.key, offset: 4, type: .text)
      let selection2 = RangeSelection(anchor: startPoint2, focus: endPoint2, format: TextFormat())
      try updateSelectionResolveTextNodes(selection: selection2)
      XCTAssertEqual(selection2.focus.offset, 4)

      let startPoint3 = createPoint(key: paragraphNode.key, offset: 3, type: .element)
      let endPoint3 = createPoint(key: paragraphNode1.key, offset: 4, type: .element)
      let selection3 = RangeSelection(anchor: startPoint3, focus: endPoint3, format: TextFormat())
      try updateSelectionResolveTextNodes(selection: selection3)
      XCTAssertEqual(selection3.focus.offset, 13)
    }
  }

  func testMakeRangeSelection() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      createExampleNodeTree()
    }

    view.textView.selectedRange = NSRange(location: 0, length: 5)

    try editor.update {
      let newSelection = try makeRangeSelection(
        anchorKey: "1",
        anchorOffset: 0,
        focusKey: "1",
        focusOffset: 5,
        anchorType: .text,
        focusType: .text)

      guard let selection = editor.testing_getPendingEditorState()?.selection as? RangeSelection else {
        XCTFail("Expected range selection")
        return
      }

      XCTAssertEqual(newSelection.anchor.key, "1")
      XCTAssertEqual(selection.anchor.key, "1")
      XCTAssertEqual(selection.focus.key, "1")
      XCTAssertEqual(selection.anchor.offset, 0)
      XCTAssertEqual(selection.focus.offset, 5)
      XCTAssertEqual(selection.anchor.type, SelectionType.text)
      XCTAssertEqual(selection.focus.type, SelectionType.text)
    }
  }

  func testRangeSelectionEquality() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      createExampleNodeTree()
    }

    try editor.update {
      let newSelection = try makeRangeSelection(
        anchorKey: "1",
        anchorOffset: 0,
        focusKey: "1",
        focusOffset: 5,
        anchorType: .text,
        focusType: .text)

      editor.getEditorState().selection = newSelection
    }

    try editor.update {
      let newSelection2 = try makeRangeSelection(
        anchorKey: "1",
        anchorOffset: 0,
        focusKey: "1",
        focusOffset: 5,
        anchorType: .text,
        focusType: .text
      )

      XCTAssertTrue(editor.getEditorState().selection?.isSelection(newSelection2) ?? false)

      var textFormat = TextFormat()
      textFormat.bold = true
      newSelection2.format = textFormat

      XCTAssertFalse(editor.getEditorState().selection?.isSelection(newSelection2) ?? false)
    }
  }

  func testTransferStartingElementPointToTextPoint() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      let textNode = TextNode()
      try textNode.setText("hello")

      let textNode2 = TextNode()
      try textNode2.setText("world")

      let paragraphNode = ParagraphNode()
      try paragraphNode.append([textNode])
      try paragraphNode.append([textNode2])

      let startPoint = createPoint(key: paragraphNode.key, offset: 0, type: .text)
      let endPoint = createPoint(key: paragraphNode.key, offset: 0, type: .text)

      XCTAssertNoThrow(
        try transferStartingElementPointToTextPoint(
          start: startPoint,
          end: endPoint,
          format: TextFormat()
        )
      )
    }
  }

  func testUpdateCaretSelectionForUnicodeCharacter() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      let textNode = TextNode()
      try textNode.setText("hello ")

      let textNode2 = TextNode()
      try textNode2.setText("world")

      let paragraphNode = ParagraphNode()
      try paragraphNode.append([textNode, textNode2])

      let startPoint = createPoint(key: textNode.key, offset: 0, type: .text)
      let endPoint = createPoint(key: textNode.key, offset: 6, type: .text)
      let selection = RangeSelection(anchor: startPoint, focus: endPoint, format: TextFormat())

      XCTAssertEqual(startPoint.offset, 0)
      XCTAssertEqual(endPoint.offset, 6)

      XCTAssertNoThrow(
        try updateCaretSelectionForUnicodeCharacter(
          selection: selection,
          isBackward: false
        )
      )

      XCTAssertEqual(startPoint.offset, 5)
      XCTAssertEqual(endPoint.offset, 6)
    }
  }

  func testGetIndexFromPossibleClone() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      guard let editorState = getActiveEditorState(),
            let rootNode = editorState.getRootNode()
      else {
        XCTFail("should have editor state")
        return
      }

      let textNode = TextNode()
      try textNode.setText("hello ")

      let textNode2 = TextNode()
      try textNode2.setText("world")

      let textNode3 = TextNode()
      try textNode3.setText("again!")

      let paragraphNode = ParagraphNode()
      try paragraphNode.append([textNode])
      try paragraphNode.append([textNode2])
      try paragraphNode.append([textNode3])

      try rootNode.append([paragraphNode])

      guard let nodeMap = editor.testing_getPendingEditorState()?.nodeMap else {
        XCTFail("Faied to get nodeMap")
        return
      }

      XCTAssertEqual(getIndexFromPossibleClone(node: textNode, parent: paragraphNode, nodeMap: nodeMap), 0)
      XCTAssertEqual(getIndexFromPossibleClone(node: textNode2, parent: paragraphNode, nodeMap: nodeMap), 1)
      XCTAssertEqual(getIndexFromPossibleClone(node: textNode3, parent: paragraphNode, nodeMap: nodeMap), 2)

      XCTAssertEqual(getIndexFromPossibleClone(node: textNode, parent: rootNode, nodeMap: nodeMap), 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.
 */

@testable import Lexical
import XCTest

class SerializationTests: XCTestCase {

  var view: LexicalView?
  var editor: Editor {
    get {
      guard let editor = view?.editor else {
        XCTFail("Editor unexpectedly nil")
        fatalError()
      }
      return editor
    }
  }

  override func setUp() {
    view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
  }

  override func tearDown() {
    view = nil
  }

  func testBuiltinNodeMap() throws {
    let mapping = makeDeserializationMap()

    XCTAssertNotNil(mapping[NodeType.text], "Text Node should not be nil")
    XCTAssertNotNil(mapping[NodeType.paragraph], "Paragraph Node should not be nil")
    XCTAssertNotNil(mapping[NodeType.root], "Root Node should not be nil")
    XCTAssertNotNil(mapping[NodeType.element], "Element Node should not be nil")
    XCTAssertNotNil(mapping[NodeType.quote], "Quote Node should not be nil")
    XCTAssertNotNil(mapping[NodeType.heading], "Heading Node should not be nil")
  }

  func testSimpleSerialization() throws {
    try editor.update {
      guard let editorState = getActiveEditorState(),
            let rootNode = editorState.getRootNode()
      else {
        XCTFail("should have editor state")
        return
      }

      guard let paragraphNode = rootNode.getFirstChild() as? ParagraphNode else {
        XCTFail()
        return
      }

      let textNode = TextNode(text: "Hello")
      try paragraphNode.append([textNode])
    }

    try editor.update {
      let nodes = try generateArrayFromSelectedNodes(editor: editor, selection: nil).nodes

      let encoder = JSONEncoder()
      let decoder = JSONDecoder()

      let result: Data = try encoder.encode(nodes)

      XCTAssertNotNil(result)

      let newSerialization: SerializedNodeArray = try decoder.decode(SerializedNodeArray.self, from: result)
      XCTAssertEqual(newSerialization.nodeArray.count, 1)
      XCTAssertEqual((newSerialization.nodeArray[0] as? ElementNode)?.children.count, 1)

      guard let textNode = (newSerialization.nodeArray[0] as? ElementNode)?.getChildren()[0] as? TextNode else {
        XCTFail("Could not find TextNode")
        return
      }

      XCTAssertEqual(textNode.getTextContent(), "Hello")
    }
  }

  let jsonString = "{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"This is \",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":1,\"mode\":\"normal\",\"style\":\"\",\"text\":\"bold\",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\" \",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":2,\"mode\":\"normal\",\"style\":\"\",\"text\":\"italic\",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\" \",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":8,\"mode\":\"normal\",\"style\":\"\",\"text\":\"underline\",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\" text in the first paragraph.\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"paragraph\",\"version\":1},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"This is another paragraph.\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"paragraph\",\"version\":1},{\"children\":[{\"detail\":0,\"format\":16,\"mode\":\"normal\",\"style\":\"\",\"text\":\"This is a code line.\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"paragraph\",\"version\":1},{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"This is \",\"type\":\"text\",\"version\":1},{\"detail\":0,\"format\":4,\"mode\":\"normal\",\"style\":\"\",\"text\":\"strikethrough\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"paragraph\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}"

  func testWebFormatJSONImporting() throws {
    try editor.update {
      let decoder = JSONDecoder()
      let decodedNodeArray = try decoder.decode(SerializedEditorState.self, from: (jsonString.data(using: .utf8) ?? Data()))
      guard let rootNode = decodedNodeArray.rootNode else {
        XCTFail("Failed to decode RootNode")
        return
      }

      guard let selection = try getSelection() as? RangeSelection else {
        XCTFail("Could not get selection")
        return
      }

      _ = try insertGeneratedNodes(editor: editor, nodes: rootNode.getChildren(), selection: selection)
    }

    try editor.read {
      let rootNode = editor.getEditorState().getRootNode()
      XCTAssertEqual(rootNode?.children.count, 4)

      guard let firstParagraph = rootNode?.getChildren()[0] as? ParagraphNode else {
        XCTFail("Could not get first ParagraphNode")
        return
      }
      XCTAssertEqual(firstParagraph.children.count, 7)
      XCTAssertEqual(firstParagraph.getTextContent(), "This is bold italic underline text in the first paragraph.\n")
      XCTAssertTrue((firstParagraph.getChildren()[1] as? TextNode)?.format.bold ?? false)
      XCTAssertTrue((firstParagraph.getChildren()[3] as? TextNode)?.format.italic ?? false)
      XCTAssertTrue((firstParagraph.getChildren()[5] as? TextNode)?.format.underline ?? false)

      guard let secondPargraph = rootNode?.getChildren()[1] as? ParagraphNode else {
        XCTFail("Could not get second ParagraphNode")
        return
      }
      XCTAssertEqual(secondPargraph.children.count, 1)
      XCTAssertEqual(secondPargraph.getTextContent(), "This is another paragraph.\n")

      guard let childrenSize = rootNode?.getChildrenSize(), childrenSize >= 3, let thirdParagraph = rootNode?.getChildren()[2] as? ParagraphNode else {
        XCTFail("Could not get third ParagraphNode")
        return
      }
      XCTAssertEqual(thirdParagraph.children.count, 1)
      XCTAssertEqual(thirdParagraph.getTextContent(), "This is a code line.\n")
      XCTAssertTrue((thirdParagraph.getChildren().first as? TextNode)?.format.code ?? false)

      guard let fourthParagraph = rootNode?.getChildren()[3] as? ParagraphNode else {
        XCTFail("Could not get fourth ParagraphNode")
        return
      }
      XCTAssertEqual(fourthParagraph.children.count, 2)
      XCTAssertEqual(fourthParagraph.getTextContent(), "This is strikethrough")
      XCTAssertTrue((fourthParagraph.getChildren().last as? TextNode)?.format.strikethrough ?? false)
    }
  }

  func testGetTextOutOfJSONHeadlessly() throws {
    let headlessEditor = Editor.createHeadless(editorConfig: EditorConfig(theme: Theme(), plugins: []))

    var text: String?

    try headlessEditor.update {
      let decodedNodeArray = try JSONDecoder().decode(SerializedEditorState.self, from: (jsonString.data(using: .utf8) ?? Data()))

      guard let rootNode = decodedNodeArray.rootNode else {
        XCTFail("Failed to decode RootNode")
        return
      }

      guard let selection = try getSelection() as? RangeSelection else {
        XCTFail("Could not get selection")
        return
      }

      try insertGeneratedNodes(editor: headlessEditor, nodes: rootNode.getChildren(), selection: selection)

      text = getRoot()?.getTextContent()
    }

    XCTAssertEqual(text, "This is bold italic underline text in the first paragraph.\nThis is another paragraph.\nThis is a code line.\nThis is strikethrough")
  }

  func testFromToJSONMethods() throws {
    let headlessEditor = Editor.createHeadless(editorConfig: EditorConfig(theme: Theme(), plugins: []))
    let editorState = try EditorState.fromJSON(json: jsonString, editor: headlessEditor)

    try headlessEditor.read {
      XCTAssertEqual(getRoot()?.getTextContent(), "", "Expected empty string")
    }

    try editorState.read {
      let text = getRoot()?.getTextContent()
      XCTAssertEqual(text, "This is bold italic underline text in the first paragraph.\nThis is another paragraph.\nThis is a code line.\nThis is strikethrough", "Expected text in new editor state")
    }

    try headlessEditor.setEditorState(editorState)

    try headlessEditor.read {
      let text = getRoot()?.getTextContent()
      XCTAssertEqual(text, "This is bold italic underline text in the first paragraph.\nThis is another paragraph.\nThis is a code line.\nThis is strikethrough", "Expected text in editor")
    }

    let jsonResult = try editorState.toJSON()

    // test json equality
    guard let comparisonJSONData = jsonString.data(using: .utf8), let outputJSONData = jsonResult.data(using: .utf8) else {
      XCTFail("couldn't convert to data")
      return
    }

    guard let comparisonJSON = try? JSONSerialization.jsonObject(with: comparisonJSONData) as? NSDictionary, let outputJSON = try? JSONSerialization.jsonObject(with: outputJSONData) as? NSDictionary else {
      XCTFail("Could not json decode")
      return
    }

    XCTAssertEqual(comparisonJSON, outputJSON, "Equality of json")
  }
}


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

@testable import Lexical
import XCTest

class TextViewTests: XCTestCase {

  func testInitialise() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let textView = view.textView
    XCTAssertTrue(textView.textStorage is TextStorage)
    XCTAssertTrue(textView.layoutManager is LayoutManager)
    XCTAssertNotNil(textView.editor)
  }

  func testGetNativeSelection() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor
    let textView = view.textView

    // Note that modifying the text view like this will break the reconciler. That's OK in this test
    // as it doesn't run the reconciler!

    textView.text = "Hello world"
    textView.isUpdatingNativeSelection = true // disable the selection feeding back to Lexical -- in this case we _just_ want a native selection
    textView.selectedRange = NSRange(location: 1, length: 4)
    textView.isUpdatingNativeSelection = false
    XCTAssertEqual(textView.selectedRange.location, 1, "Selection range should be 1")
    XCTAssertEqual(textView.selectedRange.length, 4, "Selection length should be 4")

    let selection = editor.getNativeSelection()
    guard let range = selection.range else {
      XCTFail("selection should have range")
      return
    }
    XCTAssertEqual(range.location, 1, "Fetched native selection range should be 1")
    XCTAssertEqual(range.length, 4, "Fetched native selection length should be 4")
    XCTAssertNotNil(selection.opaqueRange)
  }

  func testMoveNativeSelection() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor
    let textView = view.textView

    textView.text = "Hello world"
    textView.isUpdatingNativeSelection = true
    textView.selectedRange = NSRange(location: 1, length: 4)
    textView.isUpdatingNativeSelection = false

    let selection = editor.getNativeSelection()
    guard let range = selection.range else {
      XCTFail("selection should have range")
      return
    }
    XCTAssertEqual(range.location, 1)
    XCTAssertEqual(range.length, 4)

    editor.moveNativeSelection(type: .extend, direction: .backward, granularity: .character)

    let modifiedSelection = editor.getNativeSelection()
    guard let modifiedRange = modifiedSelection.range else {
      XCTFail("selection should have range")
      return
    }
    XCTAssertEqual(modifiedRange.location, 0)
    XCTAssertEqual(modifiedRange.length, 5)
  }

  func testUpdateNativeSelection() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let textView = view.textView

    try textView.editor.update {
      createExampleNodeTree()
    }

    try textView.editor.getEditorState().read {
      let selection = RangeSelection(anchor: Point(key: "1", offset: 1, type: .text),
                                     focus: Point(key: "2", offset: 3, type: .text),
                                     format: TextFormat())

      try textView.updateNativeSelection(from: selection)
      XCTAssertEqual(textView.selectedRange.location, 1)
      XCTAssertEqual(textView.selectedRange.length, 8)

      let selection2 = RangeSelection(anchor: Point(key: "7", offset: 0, type: .element),
                                      focus: Point(key: "7", offset: 1, type: .element),
                                      format: TextFormat())

      try textView.updateNativeSelection(from: selection2)
      XCTAssertEqual(textView.selectedRange.location, 52)
      XCTAssertEqual(textView.selectedRange.length, 11)
    }
  }

  func testInsertTextUITextInputMethodWithNewLine() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let textView = view.textView

    try textView.editor.update {
      guard let rootNode = getActiveEditorState()?.getRootNode(), let paragraphNode = rootNode.getFirstChild() as? ParagraphNode else {
        XCTFail("No root node")
        return
      }
      let textNode = TextNode()
      try textNode.setText("Hello world")
      try paragraphNode.append([textNode])
      let anchor = createPoint(key: "1", offset: 11, type: .text)
      let focus = createPoint(key: "1", offset: 11, type: .text)
      textView.editor.getEditorState().selection = RangeSelection(anchor: anchor, focus: focus, format: TextFormat())
    }

    textView.selectedRange = NSRange(location: 11, length: 0)

    textView.insertText("\n")
    XCTAssertEqual(textView.text, "Hello world\n", "Should have inserted character in non-controlled mode")
    if let newParagraphNode = getNodeByKey(key: "2") as? ParagraphNode {
      XCTAssertEqual(newParagraphNode.key, "2")
      XCTAssertEqual(newParagraphNode.parent, kRootNodeKey)
      XCTAssertEqual(newParagraphNode.getChildren(), [])
    }

    textView.insertText("Hey")
    XCTAssertEqual(textView.text, "Hello world\nHey", "Should have inserted character in controller mode")
    if let newTextNode = getNodeByKey(key: "3") as? TextNode {
      XCTAssertEqual(newTextNode.key, "3")
      XCTAssertEqual(newTextNode.parent, "2")
      XCTAssertEqual(newTextNode.getTextPart(), "Hey")
    }

    guard let selection = textView.editor.getEditorState().selection as? RangeSelection else {
      XCTFail("Expected range selection")
      return
    }
    XCTAssertEqual(selection.anchor.key, "3")
    XCTAssertEqual(selection.focus.key, "3")
    XCTAssertEqual(selection.anchor.offset, 3)
    XCTAssertEqual(selection.focus.offset, 3)
    XCTAssertEqual(selection.anchor.type, SelectionType.text)
  }

  // Test disabled due to iOS 16 UIPasteboard restrictions. I can't figure out a workaround right now. @amyworrall
  //  func testCut() throws {
  //    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
  //    let textView = view.textView
  //
  //    textView.insertText("Hello world")
  //    let anchor = createPoint(key: "1", offset: 6, type: .text)
  //    let focus = createPoint(key: "1", offset: 11, type: .text)
  //    textView.editor.getEditorState().selection = RangeSelection(anchor: anchor, focus: focus, format: TextFormat())
  //
  //    textView.cut(nil)
  //
  //    try textView.editor.update {
  //      let itemSet = UIPasteboard.general.itemSet(withPasteboardTypes: ["x-lexical-nodes"])
  //      guard let data = UIPasteboard.general.data(forPasteboardType: "x-lexical-nodes", inItemSet: itemSet)?.last else {
  //        print("No data on pasteboard")
  //        return
  //      }
  //
  //      let json = try JSONDecoder().decode(SerializedNodeArray.self, from: data)
  //      if let node = json.nodeArray.first as? TextNode {
  //        let text = node.getText_dangerousPropertyAccess()
  //        XCTAssertEqual(String(describing: text), "world")
  //      } else {
  //        XCTFail("First (only) node in nodeArray was not TextNode")
  //      }
  //    }
  //  }

  // Test disabled due to iOS 16 UIPasteboard restrictions. I can't figure out a workaround right now. @amyworrall
  //  func testCopy() throws {
  //    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
  //    let textView = view.textView
  //
  //    textView.insertText("Hello world")
  //    let anchor = createPoint(key: "1", offset: 6, type: .text)
  //    let focus = createPoint(key: "1", offset: 11, type: .text)
  //    textView.editor.getEditorState().selection = RangeSelection(anchor: anchor, focus: focus, format: TextFormat())
  //
  //    textView.copy(nil)
  //
  //    try textView.editor.update {
  //      let itemSet = UIPasteboard.general.itemSet(withPasteboardTypes: ["x-lexical-nodes"])
  //      guard let data = UIPasteboard.general.data(forPasteboardType: "x-lexical-nodes", inItemSet: itemSet)?.last else {
  //        print("No data on pasteboard")
  //        return
  //      }
  //
  //      let json = try JSONDecoder().decode(SerializedNodeArray.self, from: data)
  //      if let node = json.nodeArray.first as? TextNode {
  //        let text = node.getText_dangerousPropertyAccess()
  //        XCTAssertEqual(String(describing: text), "world")
  //      } else {
  //        XCTFail("First (only) node in nodeArray was not TextNode")
  //      }
  //    }
  //  }

  func testInsertPlainText() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let textView = view.textView

    textView.insertText("Hello world")
    let anchor = createPoint(key: "1", offset: 11, type: .text)
    let focus = createPoint(key: "1", offset: 11, type: .text)
    textView.editor.getEditorState().selection = RangeSelection(anchor: anchor, focus: focus, format: TextFormat())

    guard let selection = textView.editor.getEditorState().selection as? RangeSelection else {
      XCTFail("Need selection")
      return
    }

    try textView.editor.update {
      let text = "Text\nText"

      try insertPlainText(selection: selection, text: text)
    }

    try textView.editor.update {
      let nodemap = textView.editor.getEditorState().nodeMap
      print(nodemap)
      XCTAssertTrue((nodemap["0"] as? ParagraphNode)?.children.count == 1)
      XCTAssertTrue((nodemap["1"] as? TextNode)?.getTextPart() == "Hello worldText")
      XCTAssertTrue((nodemap["3"] as? TextNode)?.getTextPart() == "Text")
      XCTAssertTrue((nodemap["4"] as? ParagraphNode)?.children.count == 1)
    }
  }

  func testInsertPlainTextWithinternalNewlines() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let textView = view.textView

    textView.insertText("Hello world")
    let anchor = createPoint(key: "1", offset: 11, type: .text)
    let focus = createPoint(key: "1", offset: 11, type: .text)
    textView.editor.getEditorState().selection = RangeSelection(anchor: anchor, focus: focus, format: TextFormat())

    guard let selection = textView.editor.getEditorState().selection as? RangeSelection else {
      XCTFail("Need selection")
      return
    }

    try textView.editor.update {
      let text = "Text\n\n\n\nText"

      try insertPlainText(selection: selection, text: text)
    }

    try textView.editor.update {
      let nodemap = textView.editor.getEditorState().nodeMap
      print(nodemap)
      XCTAssertTrue((nodemap["0"] as? ParagraphNode)?.children.count == 1)
      XCTAssertTrue((nodemap["1"] as? TextNode)?.getTextPart() == "Hello worldText")
      XCTAssertTrue((nodemap["4"] as? ParagraphNode)?.children.count == 0)
      XCTAssertTrue((nodemap["6"]as? ParagraphNode)?.children.count == 0)
      XCTAssertTrue((nodemap["8"] as? ParagraphNode)?.children.count == 0)
      XCTAssertTrue((nodemap["10"] as? ParagraphNode)?.children.count == 1)
      XCTAssertTrue((nodemap["9"] as? TextNode)?.getTextPart() == "Text")
    }
  }

  func testInsertRTF() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let textView = view.textView

    textView.insertText("Hello world")
    let anchor = createPoint(key: "1", offset: 11, type: .text)
    let focus = createPoint(key: "1", offset: 11, type: .text)
    textView.editor.getEditorState().selection = RangeSelection(anchor: anchor, focus: focus, format: TextFormat())

    guard let selection = textView.editor.getEditorState().selection as? RangeSelection else {
      XCTFail("Need selection")
      return
    }

    try textView.editor.update {
      let text = NSMutableAttributedString(string: "Test\nText")
      text.addAttribute(.underlineStyle, value: NSUnderlineStyle.single, range: NSRange(location: 0, length: text.length))

      try insertRTF(selection: selection, attributedString: text)
    }

    try textView.editor.update {
      let nodemap = textView.editor.getEditorState().nodeMap
      print(nodemap)
      XCTAssertTrue((nodemap["0"] as? ParagraphNode)?.children.count == 2)
      XCTAssertTrue((nodemap["1"] as? TextNode)?.getTextPart() == "Hello world")
      XCTAssertTrue((nodemap["2"] as? TextNode)?.getTextPart() == "Test")
      XCTAssertTrue((nodemap["2"] as? TextNode)?.format.underline ?? false)
      XCTAssertTrue((nodemap["3"] as? TextNode)?.getTextPart() == "Text")
      XCTAssertTrue((nodemap["3"] as? TextNode)?.format.underline ?? false)
      XCTAssertTrue((nodemap["4"] as? ParagraphNode)?.children.count == 1)
    }
  }

  func testShowPlaceholderTextWithPlaceholderLabel() {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let textView = view.textView
    textView.setPlaceholderText("Enter Text", textColor: .lightGray, font: .systemFont(ofSize: 8))

    if let label = textView.subviews.first(where: { $0 is UILabel }) as? UILabel {
      XCTAssertTrue(!label.isHidden)
    }
  }

  func testShowPlaceholderTextWithPlaceholderLabelHidden() {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let textView = view.textView

    textView.setPlaceholderText("Enter Text", textColor: .lightGray, font: .systemFont(ofSize: 8))
    textView.insertText("hello")
    textView.showPlaceholderText()

    if let label = textView.subviews.first(where: { $0 is UILabel }) as? UILabel {
      XCTAssertTrue(label.isHidden, "\(label)")
    }
  }

  func testShowPlaceholderLabelOnDeletion() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let textView = view.textView
    textView.setPlaceholderText("Aa", textColor: .lightGray, font: .systemFont(ofSize: 8))

    textView.insertText("H")
    textView.showPlaceholderText()
    if let label = textView.subviews.first(where: { $0 is UILabel }) as? UILabel {
      XCTAssertTrue(label.isHidden)
    }

    try textView.editor.update {
      try onDeleteBackwardsFromUITextView(editor: textView.editor)
    }
    if let label = textView.subviews.first(where: { $0 is UILabel }) as? UILabel {
      XCTAssertTrue(!label.isHidden)
    }
  }

  func testBasicInsertStrategy() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let textView = view.textView
    textView.insertText("hello")
    textView.insertText("\n")
    textView.insertText("world")
    let nodeMap = textView.editor.getEditorState().nodeMap

    guard let selection = textView.editor.getEditorState().selection as? RangeSelection else {
      XCTFail("Need selection")
      return
    }

    try textView.editor.update {
      let nodes = nodeMap.compactMap({ $0.value }).filter({ isElementNode(node: $0) })
      _ = try insertGeneratedNodes(editor: textView.editor, nodes: nodes, selection: selection)
      XCTAssertTrue(nodes.count == 3)
      XCTAssertTrue((nodeMap["0"] as? ParagraphNode)?.children.count == 1)
    }
  }

  func testInsertEllipsis() throws {
    // iOS handles an ellipsis autocorrection by calling replaceCharacters(in:with:) on the text storage twice, each
    // time replacing one of the previous dots with empty string, then finally calling insertText on the text view
    // to insert the ellipsis. Let's simulate this.

    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let textView = view.textView
    guard let textStorage = textView.textStorage as? TextStorage else { XCTFail(); return }

    textView.insertText("He..")
    textStorage.replaceCharacters(in: NSRange(location: 3, length: 1), with: NSAttributedString(string: ""))
    textStorage.replaceCharacters(in: NSRange(location: 2, length: 1), with: NSAttributedString(string: ""))
    textView.insertText("â€¦")
    XCTAssertEqual(textView.text, "Heâ€¦")
  }
}


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

@testable import Lexical
import XCTest

class TransformTests: XCTestCase {
  static let infiniteTransformKey = "I"
  static let combinedTransformKey = "G"
  static let terminalTransform = "Z"
  static let lastTransformKey = "F"
  static let neverTransformKey = "?" // This should never run and is only used for certain optionals
  static let simpleTransformKeys = ["A", "B", "C", "D", "E"]
  var transformCount: [String: Int] = [:]
  var updateLog: [String] = []

  var teardowns: [() -> Void] = []

  var view: LexicalView?
  var editor: Editor {
    get {
      guard let editor = view?.editor else {
        XCTFail("Editor unexpectedly nil")
        fatalError()
      }
      return editor
    }
  }

  override func setUp() {
    view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())

    // To test the transform logic, we want to test the order of the logic of the transforms
    // A-E: Adds an entry to the log with the name "[A-F][Number of update]"
    // F: The last transform in the above grouping produces this text
    // G: Combines FG -> Z, ending a chain of transforms
    // I: Replaces I with itself, causing an infinite loop.

    let transforms = TransformTests.simpleTransformKeys

    for (index, key) in transforms.enumerated() {
      let nextKey = index < transforms.count - 1 ? transforms[index + 1] : TransformTests.lastTransformKey

      let teardown = editor.addNodeTransform(nodeType: NodeType.text, transform: { [weak self] node in
        guard let strongSelf = self else {
          XCTFail("strongSelf reference not found for text transform")
          return
        }

        guard let textNode = node as? TextNode else {
          throw LexicalError.invariantViolation("Text transform run on non-text node")
        }

        let textPart = textNode.getTextPart()

        if textPart.contains(key) {
          let count = (strongSelf.transformCount[key] ?? 0) + 1

          try textNode.setText(textPart.replacingOccurrences(of: key, with: nextKey))

          strongSelf.transformCount[key] = count
          strongSelf.updateLog.append("\(key)\(count)_start")
        }
      })

      teardowns.append(teardown)
    }

    let infiniteTransform = editor.addNodeTransform(nodeType: NodeType.text, transform: { [weak self] node in
      guard let strongSelf = self else {
        XCTFail("strongSelf reference not found for text transform")
        return
      }

      guard let textNode = node as? TextNode else {
        throw LexicalError.invariantViolation("Text transform run on non-text node")
      }

      let textPart = textNode.getTextPart()

      if textPart.contains(TransformTests.infiniteTransformKey) {
        let count = (strongSelf.transformCount[TransformTests.infiniteTransformKey] ?? 0) + 1

        try textNode.setText(textPart)
        strongSelf.transformCount[TransformTests.infiniteTransformKey] = count
        strongSelf.updateLog.append("\(TransformTests.infiniteTransformKey)\(count)_start")
      }
    })

    let combinedTransform = editor.addNodeTransform(nodeType: NodeType.text, transform: { [weak self] node in
      guard let strongSelf = self else {
        XCTFail("strongSelf reference not found for text transform")
        return
      }

      guard let textNode = node as? TextNode else {
        throw LexicalError.invariantViolation("Text transform run on non-text node")
      }

      let textPart = textNode.getTextPart()

      let combinedText = "\(TransformTests.lastTransformKey)\(TransformTests.combinedTransformKey)"

      if textPart.contains(combinedText) {
        let count = (strongSelf.transformCount[TransformTests.combinedTransformKey] ?? 0) + 1

        try textNode.setText(textPart.replacingOccurrences(of: combinedText, with: TransformTests.terminalTransform))

        strongSelf.transformCount[TransformTests.combinedTransformKey] = count
        strongSelf.updateLog.append("\(TransformTests.combinedTransformKey)\(count)_start")
      }
    })

    teardowns.append(contentsOf: [infiniteTransform, combinedTransform])
  }

  override func tearDown() {
    for teardown in teardowns {
      teardown()
    }

    view = nil
  }

  // MARK: - Simple update tests

  func testNoUpdateOnCleanEditor() throws {
    try editor.update {}

    XCTAssert(updateLog.isEmpty, "Transforms were called when no updates occurred")
  }

  func testInfiniteTransformLoop() throws {
    try editor.update {
      guard let root = getActiveEditorState()?.getRootNode() else {
        XCTFail("Couldn't get root node")
        return
      }
      let paragraph = ParagraphNode()
      let text = TextNode(text: TransformTests.infiniteTransformKey)

      try root.append([paragraph])
      try paragraph.append([text])
    }

    XCTAssert(updateLog.count <= Editor.maxUpdateCount, "Did not prevent infinite update")
  }

  func testLinearlyDependentTransforms() throws {
    try editor.update {
      guard let root = getActiveEditorState()?.getRootNode() else {
        XCTFail("Couldn't get root node")
        return
      }
      let paragraph = ParagraphNode()
      let text = TextNode(text: TransformTests.simpleTransformKeys[0])

      try root.append([paragraph])
      try paragraph.append([text])
    }

    XCTAssert(updateLog.count == TransformTests.simpleTransformKeys.count, "Did not execute expected number of transforms")
  }

  func testSynchronouslyDependentTransforms() throws {
    try editor.update {
      guard let root = getActiveEditorState()?.getRootNode() else {
        XCTFail("Couldn't get root node")
        return
      }
      let paragraph = ParagraphNode()
      let text = TextNode(text: TransformTests.simpleTransformKeys[0])

      try root.append([paragraph])
      try paragraph.append([text])
    }

    XCTAssert(updateLog.count == TransformTests.simpleTransformKeys.count, "Did not execute expected number of transforms")

    for (index, transform) in TransformTests.simpleTransformKeys.enumerated() {
      XCTAssertTrue(updateLog[index].contains(transform), "Transforms were not executed in expected order")
    }
  }

  func testBranchingDependentTransforms() throws {
    try editor.update {
      guard let root = getActiveEditorState()?.getRootNode() else {
        XCTFail("Couldn't get root node")
        return
      }
      let paragraph = ParagraphNode()
      let text = TextNode(text: "\(TransformTests.simpleTransformKeys[0])\(TransformTests.combinedTransformKey)")

      try root.append([paragraph])
      try paragraph.append([text])
    }

    XCTAssert(updateLog.contains(where: { $0.contains(TransformTests.combinedTransformKey) }), "Did not execute dependent transform")
  }
}


/*
 * 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
@testable import Lexical
import XCTest

class UnknownNodeTests: XCTestCase {

  var textView: TextView?
  var editor: Editor? {
    get {
      return textView?.editor
    }
  }

  override func setUp() {
    textView = TextView()
  }

  override func tearDown() {
    textView = nil
  }

  func testCanEncodeAndDecodeAnyJson() throws {
    let simpleJson = """
{"id": "test-id","type": "a-random-string", "randomValue": [1, "2", true, false, { "3": 4.5 }, null]}
"""

    let decoder = JSONDecoder()
    let encoder = JSONEncoder()

    guard let data = simpleJson.data(using: .utf8) else {
      XCTFail("No data created")
      return
    }

    let node = try decoder.decode(UnknownNode.self, from: data)

    let newData = try encoder.encode(node)

    let newNode = try decoder.decode(UnknownNode.self, from: newData)

    XCTAssertEqual(node.data, newNode.data, "Same data was not decoded in node constructors")
    XCTAssertEqual(node, newNode, "Node failed equality check from node constructors")
  }
}


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

@testable import Lexical
import XCTest

class UpdatesTests: XCTestCase {

  func testUpdateNodeMap() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor
    var node: Node?

    try editor.update {
      let editorState = getActiveEditorState()
      XCTAssertNotNil(editorState)
      node = Node()

      guard let node else {
        XCTFail("should have node")
        return
      }

      guard let paragraphNode = getRoot()?.getFirstChild() as? ParagraphNode else {
        XCTFail()
        return
      }

      try paragraphNode.append([node])
    }

    XCTAssertNotNil(node)
    try editor.getEditorState().read {
      guard let editorState = getActiveEditorState() else {
        XCTFail("should have editor state")
        return
      }
      XCTAssertTrue(editorState.nodeMap["1"] === node)
    }
  }

  func testEditorStateNotNil() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.getEditorState().read {
      let editorState = getActiveEditorState()
      XCTAssertNotNil(editorState)
    }
  }

  func testUpdateListenersIsFired() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      createExampleNodeTree()
    }

    var listenerCount = 0
    var updateCount = 0

    _ = editor.registerUpdateListener(listener: { editorState, previousEditorState, dirtyNodes in
      listenerCount += 1
    })

    XCTAssertTrue(listenerCount == 0)
    XCTAssertTrue(updateCount == 0)

    try editor.update {
      updateCount += 1
    }
    try editor.update {
      updateCount += 1
    }

    XCTAssertTrue(listenerCount == 2)
    XCTAssertTrue(updateCount == 2)
  }

  func testUpdateListenersCanBeUnsubscribed() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      createExampleNodeTree()
    }

    var listenerCount = 0
    var updateCount = 0

    let unsubscribe = editor.registerUpdateListener(listener: { editorState, previousEditorState, dirtyNodes in
      listenerCount += 1
    })

    try editor.update {
      updateCount += 1
    }
    try editor.update {
      updateCount += 1
    }

    XCTAssertTrue(listenerCount == 2)
    XCTAssertTrue(updateCount == 2)

    unsubscribe()

    try editor.update {
      updateCount += 1
    }
    try editor.update {
      updateCount += 1
    }

    XCTAssertTrue(listenerCount == 2)
    XCTAssertTrue(updateCount == 4)
  }

  func testTextContentListenersOnlyFireOnTextContentChanges() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      createExampleNodeTree()
    }

    var updateListenerCount = 0
    var textContentListenerCount = 0
    var updateCount = 0

    _ = editor.registerUpdateListener(listener: { editorState, previousEditorState, dirtyNodes in
      updateListenerCount += 1
    })
    _ = editor.registerTextContentListener(listener: { latestTextContent in
      textContentListenerCount += 1
    })

    XCTAssertTrue(updateListenerCount == 0)
    XCTAssertTrue(textContentListenerCount == 0)
    XCTAssertTrue(updateCount == 0)

    try editor.update {
      updateCount += 1
    }
    try editor.update {
      updateCount += 1
    }

    XCTAssertTrue(updateListenerCount == 2)
    XCTAssertTrue(textContentListenerCount == 0)
    XCTAssertTrue(updateCount == 2)

    try editor.update {
      guard let rootNode = getActiveEditorState()?.getRootNode(), let paragraphNode = rootNode.getFirstChild() as? ParagraphNode else {
        XCTFail("No root node")
        return
      }

      updateCount += 1

      let textNode = TextNode()
      try textNode.setText("Hello world 2")
      try paragraphNode.append([textNode])
    }

    XCTAssertTrue(updateListenerCount == 3)
    XCTAssertTrue(textContentListenerCount == 1)
    XCTAssertTrue(updateCount == 3)
  }

  func testCommandListenersFireWhenDispatchCommandIsCalled() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      createExampleNodeTree()
    }

    let validCommandType = CommandType.click
    let invalidCommandtype = CommandType.deleteLine

    var dispatchCount = 0
    var validCommandTypeCount = 0

    _ = editor.registerCommand(type: validCommandType, listener: { payload in
      validCommandTypeCount += 1

      return true
    })

    editor.dispatchCommand(type: invalidCommandtype, payload: nil)
    editor.dispatchCommand(type: validCommandType, payload: nil)
    editor.dispatchCommand(type: invalidCommandtype, payload: nil)

    dispatchCount += 3

    XCTAssertTrue(dispatchCount == 3)
    XCTAssertTrue(validCommandTypeCount == 1)
  }

  func testCommandListenersFiredInPriorityOrder() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editor = view.editor

    try editor.update {
      createExampleNodeTree()
    }

    var commandListenerCount = 0

    _ = editor.registerCommand(type: CommandType.click, listener: { payload in
      commandListenerCount += 1

      XCTAssertEqual(commandListenerCount, 2)

      return true
    },
    priority: CommandPriority.Editor
    )
    _ = editor.registerCommand(type: CommandType.click, listener: {  payload in
      commandListenerCount += 1

      XCTAssertEqual(commandListenerCount, 1)

      return true
    },
    priority: CommandPriority.High
    )

    editor.dispatchCommand(type: CommandType.click, payload: 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.
 */

@testable import Lexical
import XCTest

class UtilsTests: XCTestCase {
  func testInitializeEditor() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    let editorState = view.editor.getEditorState()

    guard let rootNode = editorState.nodeMap[kRootNodeKey] as? RootNode else {
      XCTFail("Failed to create editor state and rootNode")
      return
    }

    guard let selection = editorState.selection as? RangeSelection else {
      XCTFail("Expected range selection")
      return
    }

    XCTAssertEqual(rootNode.children.count, 1, "Expected 1 child")
    XCTAssertEqual(rootNode.children[0], "0")

    guard let paragraphNode = getNodeByKey(key: "0") else { return }

    XCTAssertEqual(paragraphNode.parent, kRootNodeKey)
    XCTAssertNotNil(selection)
    XCTAssertEqual(selection.anchor.key, "0")
    XCTAssertEqual(selection.focus.key, "0")
    XCTAssertEqual(selection.anchor.type, SelectionType.element)
    XCTAssertEqual(selection.focus.type, SelectionType.element)
    XCTAssertEqual(selection.anchor.offset, 0)
    XCTAssertEqual(selection.focus.offset, 0)
  }

  func testDefaultClearEditor() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())

    try view.editor.update {
      createExampleNodeTree()

      guard let root = getNodeByKey(key: kRootNodeKey) as? RootNode else { return }

      XCTAssertEqual(root.getChildren().count, 4, "Root should have 4 children")
    }

    try view.textView.defaultClearEditor()

    try view.editor.read {
      guard let root = getNodeByKey(key: kRootNodeKey) as? RootNode else { return }

      XCTAssertEqual(root.getChildren().count, 1, "Root should have one child")
    }
  }

  func testSelectionAfterInsertText() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    try onInsertTextFromUITextView(text: "Hello", editor: view.editor)
    guard let selection = view.editor.getEditorState().selection as? RangeSelection else {
      XCTFail("Expected range selection")
      return
    }
    XCTAssertEqual(selection.anchor.offset, 5, "Selection offset should be 5")
  }

  func testGetSelectionAfterInsertText() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())
    try onInsertTextFromUITextView(text: "Hello", editor: view.editor)
    try view.editor.read {
      guard let selection = try getSelection() as? RangeSelection else {
        XCTFail("Expected range selection")
        return
      }
      XCTAssertEqual(selection.anchor.offset, 5, "Selection offset should be 5")
    }
  }

  func testGetSelectionData() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())

    try onInsertTextFromUITextView(text: "Hello", editor: view.editor)
    let selection = try getSelectionData(editorState: view.editor.getEditorState())
    XCTAssertNotNil(selection)
    // selection should be at offset = 5 after "Hello gets inserted"
    let offset = selection.contains("5")
    XCTAssertTrue(offset, "Selection does not contain '5'. \(String(describing: selection))")
  }

  func testDefaultClearEditorWithBoldSelectionFormat() throws {
    let view = LexicalView(editorConfig: EditorConfig(theme: Theme(), plugins: []), featureFlags: FeatureFlags())

    try view.editor.update {
      guard let selection = try getSelection() as? RangeSelection else {
        XCTFail("Expected range selection")
        return
      }
      try selection.formatText(formatType: .bold)
    }

    try onInsertTextFromUITextView(text: "Hello", editor: view.editor)
    var selection: RangeSelection?
    try? view.editor.read {
      guard let newSelection = try getSelection() as? RangeSelection else {
        XCTFail("Expected range selection")
        return
      }
      selection = newSelection
    }
    XCTAssertEqual(selection?.format.bold, true)

    try view.textView.defaultClearEditor()

    try? view.editor.read {
      guard let newSelection = try getSelection() as? RangeSelection else {
        XCTFail("Expected range selection")
        return
      }
      selection = newSelection
    }
    print("updatedSelection: \(selection.debugDescription)")
    XCTAssertEqual(selection?.format.bold, false)
  }
}


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

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    window = UIWindow()
    guard let window else { return false }
    window.makeKeyAndVisible()
    let viewController = ViewController()
    let navigationController = UINavigationController(rootViewController: viewController)
    window.rootViewController = navigationController
    return true
  }

  func applicationWillTerminate(_ application: UIApplication) {
    persistEditorState()
  }

  func persistEditorState() {
    guard let viewController = window?.rootViewController as? ViewController else {
      return
    }

    viewController.persistEditorState()
  }
}


/*
 * 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 Lexical
import LexicalHTML
import LexicalListPlugin
import UIKit

internal enum OutputFormat {
  case html
  case json
}

class ExportOutputViewController: UIViewController {
  var output: String = ""

  init(editor: Editor, format: OutputFormat) {
    super.init(nibName: nil, bundle: nil)
    switch format {
    case .html:
      generateHTML(editor: editor)
    case .json:
      generateJSON(editor: editor)
    }
  }

  required init?(coder: NSCoder) {
    fatalError()
  }

  func generateHTML(editor: Editor) {
    try? editor.read {
      self.output = try generateHTMLFromNodes(editor: editor, selection: nil)
    }
  }

  func generateJSON(editor: Editor) {
    let currentEditorState = editor.getEditorState()
    if let jsonString = try? currentEditorState.toJSON() {
      output = jsonString
    }
  }

  override func loadView() {
    super.loadView()

    let textView = UITextView(frame: view.bounds)
    textView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
    textView.isEditable = false
    textView.text = output
    view.addSubview(textView)
  }
}


/*
 * 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 Lexical
import LexicalListPlugin
import UIKit

public class NodeHierarchyViewPlugin: Plugin {
  private var _hierarchyView: UITextView

  weak var editor: Editor?

  init() {
    self._hierarchyView = UITextView()
    _hierarchyView.backgroundColor = .black
    _hierarchyView.layer.borderColor = UIColor.systemGray.cgColor
    _hierarchyView.textColor = .white
    _hierarchyView.isEditable = false
    _hierarchyView.isUserInteractionEnabled = true
    _hierarchyView.isScrollEnabled = true
    _hierarchyView.font = UIFont.monospacedSystemFont(ofSize: 12, weight: .regular)
    _hierarchyView.showsVerticalScrollIndicator = true
  }

  // MARK: - Plugin API

  public func setUp(editor: Editor) {
    self.editor = editor

    _ = editor.registerUpdateListener { [weak self] activeEditorState, previousEditorState, dirtyNodes in
      if let self {
        self.updateHierarchyView(editorState: activeEditorState)
      }
    }
  }

  public func tearDown() {
  }

  public var hierarchyView: UIView {
    get {
      _hierarchyView
    }
  }

  // MARK: -

  private func updateHierarchyView(editorState: EditorState) {
    do {
      let hierarchyString = try getNodeHierarchy(editorState: editorState)
      let selectionString = try getSelectionData(editorState: editorState)
      _hierarchyView.text = "\(hierarchyString)\n\n\(selectionString)"
    } catch {
      print("Error updating node hierarchy.")
    }
  }
}


/*
 * 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 Lexical
import LexicalListPlugin
import UIKit

public class ToolbarPlugin: Plugin {
  private var _toolbar: UIToolbar

  weak var editor: Editor?
  weak var viewControllerForPresentation: UIViewController?

  init(viewControllerForPresentation: UIViewController) {
    self._toolbar = UIToolbar()
    self.viewControllerForPresentation = viewControllerForPresentation
    setUpToolbar()
  }

  // MARK: - Plugin API

  public func setUp(editor: Editor) {
    self.editor = editor

    _ = editor.registerUpdateListener { [weak self] activeEditorState, previousEditorState, dirtyNodes in
      if let self {
        self.updateToolbar()
      }
    }
  }

  public func tearDown() {
  }

  // MARK: - Public accessors

  public var toolbar: UIToolbar {
    get {
      _toolbar
    }
  }

  // MARK: - Private helpers

  var undoButton: UIBarButtonItem?
  var redoButton: UIBarButtonItem?
  var paragraphButton: UIBarButtonItem?
  var boldButton: UIBarButtonItem?
  var italicButton: UIBarButtonItem?
  var underlineButton: UIBarButtonItem?
  var strikethroughButton: UIBarButtonItem?
  var inlineCodeButton: UIBarButtonItem?
  var linkButton: UIBarButtonItem?
  var increaseIndentButton: UIBarButtonItem?
  var decreaseIndentButton: UIBarButtonItem?

  private func setUpToolbar() {
    let undo = UIBarButtonItem(image: UIImage(systemName: "arrow.uturn.backward"),
                               style: .plain,
                               target: self,
                               action: #selector(undo))
    self.undoButton = undo

    let redo = UIBarButtonItem(image: UIImage(systemName: "arrow.uturn.forward"),
                               style: .plain,
                               target: self,
                               action: #selector(redo))
    self.redoButton = redo

    let paragraph = UIBarButtonItem(image: UIImage(systemName: "paragraph"), menu: self.paragraphMenu)
    self.paragraphButton = paragraph

    let bold = UIBarButtonItem(image: UIImage(systemName: "bold"),
                               style: .plain,
                               target: self,
                               action: #selector(toggleBold))
    self.boldButton = bold

    let italic = UIBarButtonItem(image: UIImage(systemName: "italic"),
                                 style: .plain,
                                 target: self,
                                 action: #selector(toggleItalic))
    self.italicButton = italic

    let underline = UIBarButtonItem(image: UIImage(systemName: "underline"),
                                    style: .plain,
                                    target: self,
                                    action: #selector(toggleUnderline))
    self.underlineButton = underline

    let strikethrough = UIBarButtonItem(image: UIImage(systemName: "strikethrough"),
                                        style: .plain,
                                        target: self,
                                        action: #selector(toggleStrikethrough))
    self.strikethroughButton = strikethrough

    let inlineCode = UIBarButtonItem(image: UIImage(systemName: "chevron.left.forwardslash.chevron.right"),
                                     style: .plain,
                                     target: self,
                                     action: #selector(toggleInlineCode))
    self.inlineCodeButton = inlineCode

    let link = UIBarButtonItem(image: UIImage(systemName: "link"),
                               style: .plain,
                               target: self,
                               action: #selector(link))
    self.linkButton = link

    let increaseIndent = UIBarButtonItem(image: UIImage(systemName: "increase.indent"),
                                         style: .plain,
                                         target: self,
                                         action: #selector(increaseIndent))
    self.increaseIndentButton = link

    let decreaseIndent = UIBarButtonItem(image: UIImage(systemName: "decrease.indent"),
                                         style: .plain,
                                         target: self,
                                         action: #selector(decreaseIndent))
    self.decreaseIndentButton = link

    toolbar.items = [/* undo, redo, */ paragraph, bold, italic, underline, strikethrough, inlineCode /*, link */, decreaseIndent, increaseIndent]
  }

  private func updateToolbar() {
    if let selection = try? getSelection() as? RangeSelection {
      guard let anchorNode = try? selection.anchor.getNode() else { return }

      var element =
        isRootNode(node: anchorNode)
        ? anchorNode
        : findMatchingParent(startingNode: anchorNode, findFn: { e in
          let parent = e.getParent()
          return parent != nil && isRootNode(node: parent)
        })

      if element == nil {
        element = anchorNode.getTopLevelElementOrThrow()
      }

      // derive paragraph style
      if let heading = element as? HeadingNode {
        if heading.getTag() == .h1 {
          paragraphButton?.image = UIImage(named: "h1")
        } else {
          paragraphButton?.image = UIImage(named: "h2")
        }
      } else if element is CodeNode {
        paragraphButton?.image = UIImage(systemName: "chevron.left.forwardslash.chevron.right")
      } else if element is QuoteNode {
        paragraphButton?.image = UIImage(systemName: "quote.opening")
      } else if let element = element as? ListNode {
        var listType: ListType = .bullet
        if let parentList: ListNode = getNearestNodeOfType(node: anchorNode, type: .list) {
          listType = parentList.getListType()
        } else {
          listType = element.getListType()
        }
        switch listType {
        case .bullet:
          paragraphButton?.image = UIImage(systemName: "list.bullet")
        case .number:
          paragraphButton?.image = UIImage(systemName: "list.number")
        case .check:
          paragraphButton?.image = UIImage(systemName: "checklist")
        }
      } else {
        paragraphButton?.image = UIImage(systemName: "paragraph")
      }

      boldButton?.isSelected = selection.hasFormat(type: .bold)
      italicButton?.isSelected = selection.hasFormat(type: .italic)
      underlineButton?.isSelected = selection.hasFormat(type: .underline)
      strikethroughButton?.isSelected = selection.hasFormat(type: .strikethrough)
      inlineCodeButton?.isSelected = selection.hasFormat(type: .code)
    }
  }

  // MARK: - Paragraph Styles

  private var paragraphMenuItems: [UIAction] {
    return [
      UIAction(title: "Normal", image: UIImage(systemName: "paragraph"), handler: { (_) in
        self.setBlock {
          createParagraphNode()
        }
      }),
      UIAction(title: "Heading 1", image: UIImage(named: "h1"), handler: { (_) in
        self.setBlock {
          createHeadingNode(headingTag: .h1)
        }
      }),
      UIAction(title: "Heading 2", image: UIImage(named: "h2"), handler: { (_) in
        self.setBlock {
          createHeadingNode(headingTag: .h2)
        }
      }),
      UIAction(title: "Code Block", image: UIImage(systemName: "chevron.left.forwardslash.chevron.right"), handler: { (_) in
        self.setBlock {
          createCodeNode()
        }
      }),
      UIAction(title: "Quote", image: UIImage(systemName: "quote.opening"), handler: { (_) in
        self.setBlock {
          createQuoteNode()
        }
      }),
      UIAction(title: "Bulleted List", image: UIImage(systemName: "list.bullet"), handler: { (_) in
        self.editor?.dispatchCommand(type: .insertUnorderedList)
      }),
      UIAction(title: "Numbered List", image: UIImage(systemName: "list.number"), handler: { (_) in
        self.editor?.dispatchCommand(type: .insertOrderedList)
      })
    ]
  }

  private func setBlock(creationFunc: () -> ElementNode) {
    try? editor?.update {
      if let selection = try getSelection() as? RangeSelection {
        setBlocksType(selection: selection, createElement: creationFunc)
      }
    }
  }

  private var paragraphMenu: UIMenu {
    return UIMenu(title: "Paragraph Style", image: nil, identifier: nil, options: [], children: self.paragraphMenuItems)
  }

  // MARK: - Button actions

  @objc private func undo() {
  }
  @objc private func redo() {
  }

  @objc private func toggleBold() {
    editor?.dispatchCommand(type: .formatText, payload: TextFormatType.bold)
  }
  @objc private func toggleItalic() {
    editor?.dispatchCommand(type: .formatText, payload: TextFormatType.italic)
  }
  @objc private func toggleUnderline() {
    editor?.dispatchCommand(type: .formatText, payload: TextFormatType.underline)
  }
  @objc private func toggleStrikethrough() {
    editor?.dispatchCommand(type: .formatText, payload: TextFormatType.strikethrough)
  }
  @objc private func toggleInlineCode() {
    editor?.dispatchCommand(type: .formatText, payload: TextFormatType.code)
  }
  @objc private func link() {
  }
  @objc private func increaseIndent() {
    editor?.dispatchCommand(type: .indentContent, payload: nil)
  }
  @objc private func decreaseIndent() {
    editor?.dispatchCommand(type: .outdentContent, payload: 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 Lexical
import LexicalListPlugin
import UIKit

class ViewController: UIViewController {

  var lexicalView: LexicalView?
  weak var toolbar: UIToolbar?
  weak var hierarchyView: UIView?
  private let editorStatePersistenceKey = "editorState"

  override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = .systemBackground

    let toolbarPlugin = ToolbarPlugin(viewControllerForPresentation: self)
    let toolbar = toolbarPlugin.toolbar

    let hierarchyPlugin = NodeHierarchyViewPlugin()
    let hierarchyView = hierarchyPlugin.hierarchyView

    let listPlugin = ListPlugin()

    let theme = Theme()
    theme.indentSize = 40.0
    let editorConfig = EditorConfig(theme: theme, plugins: [toolbarPlugin, listPlugin, hierarchyPlugin])
    let lexicalView = LexicalView(editorConfig: editorConfig, featureFlags: FeatureFlags())

    self.lexicalView = lexicalView
    self.toolbar = toolbar
    self.hierarchyView = hierarchyView

    self.restoreEditorState()

    view.addSubview(lexicalView)
    view.addSubview(toolbar)
    view.addSubview(hierarchyView)

    navigationItem.title = "Lexical"
    setUpExportMenu()
  }

  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    if let lexicalView, let toolbar, let hierarchyView {
      let safeAreaInsets = self.view.safeAreaInsets
      let hierarchyViewHeight = 300.0

      toolbar.frame = CGRect(x: 0,
                             y: safeAreaInsets.top,
                             width: view.bounds.width,
                             height: 44)
      lexicalView.frame = CGRect(x: 0,
                                 y: toolbar.frame.maxY,
                                 width: view.bounds.width,
                                 height: view.bounds.height - toolbar.frame.maxY - safeAreaInsets.bottom - hierarchyViewHeight)
      hierarchyView.frame = CGRect(x: 0,
                                   y: lexicalView.frame.maxY,
                                   width: view.bounds.width,
                                   height: hierarchyViewHeight)
    }
  }

  func persistEditorState() {
    guard let editor = lexicalView?.editor else {
      return
    }

    let currentEditorState = editor.getEditorState()

    // turn the editor state into stringified JSON
    guard let jsonString = try? currentEditorState.toJSON() else {
      return
    }

    UserDefaults.standard.set(jsonString, forKey: editorStatePersistenceKey)
  }

  func restoreEditorState() {
    guard let editor = lexicalView?.editor else {
      return
    }

    guard let jsonString = UserDefaults.standard.value(forKey: editorStatePersistenceKey) as? String else {
      return
    }

    // turn the JSON back into a new editor state
    guard let newEditorState = try? EditorState.fromJSON(json: jsonString, editor: editor) else {
      return
    }

    // install the new editor state into editor
    try? editor.setEditorState(newEditorState)
  }

  func setUpExportMenu() {
    let menuItems = [
      UIAction(title: "Export HTML", handler: { action in
        self.showExportScreen(.html)
      }),
      UIAction(title: "Export JSON", handler: { action in
        self.showExportScreen(.json)
      })
    ]
    let menu = UIMenu(title: "Export asâ€¦", children: menuItems)
    let barButtonItem = UIBarButtonItem(title: "Export", style: .plain, target: nil, action: nil)
    barButtonItem.menu = menu
    navigationItem.rightBarButtonItem = barButtonItem
  }

  func showExportScreen(_ type: OutputFormat) {
    guard let editor = lexicalView?.editor else { return }
    let vc = ExportOutputViewController(editor: editor, format: type)
    navigationController?.pushViewController(vc, animated: 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
import Lexical
import LexicalLinkPlugin
import UIKit

struct LinkMatcherResult {
  let index: Int
  let length: Int
  let text: String
  let url: String
}

struct LinkMatcher {
  var index: Int
  var text: String
  var url: String
  var range: NSRange
}

open class AutoLinkPlugin: Plugin {

  public init() {}

  var editor: Editor?

  public func setUp(editor: Editor) {
    self.editor = editor
    do {
      try editor.registerNode(nodeType: NodeType.autoLink, class: AutoLinkNode.self)
      _ = editor.addNodeTransform(nodeType: NodeType.text, transform: { [weak self] in
        guard let strongSelf = self else { return }

        try strongSelf.transform($0)
      })

      _ = editor.addNodeTransform(nodeType: NodeType.autoLink, transform: { [weak self] linkNode in
        guard let strongSelf = self, let linkNode = linkNode as? AutoLinkNode else { return }

        try strongSelf.handleLinkEdit(linkNode: linkNode)
      })
    } catch {
      print("\(error)")
    }
  }

  public func tearDown() {
  }

  public func isAutoLinkNode(_ node: Node?) -> Bool {
    node is AutoLinkNode
  }

  public func createAutoLinkNode(url: String) -> AutoLinkNode {
    AutoLinkNode(url: url, key: nil)
  }

  func isPreviousNodeValid(node: Node) -> Bool {
    var previousNode = node.getPreviousSibling()

    if let elementNode = previousNode as? ElementNode {
      previousNode = elementNode.getLastDescendant()
    }

    if let textNode = previousNode as? TextNode {
      let text = textNode.getTextContent()
      let endIndex = text.index(before: text.endIndex)
      if String(text[endIndex]) == " " {
        return true
      }
    }

    return previousNode == nil || isLineBreakNode(previousNode)
  }

  func isNextNodeValid(node: Node) -> Bool {
    var nextNode = node.getNextSibling()

    if let elementNode = nextNode as? ElementNode {
      nextNode = elementNode.getFirstDescendant()
    }

    if let textNode = nextNode as? TextNode {
      let text = textNode.getTextContent()
      if String(text[text.startIndex]) == " " {
        return true
      }
    }

    return nextNode == nil || isLineBreakNode(nextNode)
  }

  @discardableResult
  func replaceWithChildren(node: ElementNode) throws -> [Node] {
    let children = node.getChildren()

    for child in children.reversed() {
      try node.insertAfter(nodeToInsert: child)
    }

    try node.remove()
    return children.map { child in
      child.getLatest()
    }
  }

  // MARK: - Private

  private func transform(_ node: Node) throws {
    guard
      let node = node as? TextNode,
      let parent = node.getParent()
    else { return }

    if let parent = parent as? AutoLinkNode {
      try handleLinkEdit(linkNode: parent)
    } else if !(parent is LinkNode) {
      if node.isSimpleText() {
        try handleLinkCreation(node: node)
      }

      try handleBadNeighbors(textNode: node)
    }
  }

  private func findFirstMatch(text: String) -> [LinkMatcher] {
    let emailMatcher = #"^\S+@\S+\.\S+$"#
    let urlMatcher = "((?:http|https)://)?(?:www\\.)?[\\w\\d\\-_]+\\.\\w{2,3}(\\.\\w{2})?(/(?<=/)(?:[\\w\\d\\-./_]+)?)?"
    var linkMatcher = [LinkMatcher]()

    let splitArray = text.split(separator: " ")
    // find url
    for (index, subString) in splitArray.enumerated() {
      let predicate = NSPredicate(format: "SELF MATCHES %@", argumentArray: [urlMatcher])
      if predicate.evaluate(with: String(subString)) {
        var newURLString = String(subString)
        if !newURLString.hasPrefix("https://") || !newURLString.hasPrefix("http://") {
          newURLString = "https://" + newURLString
        }

        let newNSRange = (text as NSString).range(of: String(subString))

        linkMatcher.append(LinkMatcher(index: index, text: String(subString), url: newURLString, range: newNSRange))
      }

      // find email
      let result = subString.range(of: emailMatcher, options: [.regularExpression], range: nil, locale: nil)
      if result != nil {
        let urlString = "mailto:" + String(subString)
        let newNSRange = (text as NSString).range(of: String(subString))

        linkMatcher.append(LinkMatcher(index: index, text: String(subString), url: urlString, range: newNSRange))
      }
    }

    return linkMatcher
  }

  private func handleLinkCreation(node: TextNode) throws {
    let nodeText = node.getTextContent()
    let nodeTextLength = nodeText.lengthAsNSString()
    let text = nodeText
    var textOffset = 0
    var lastNode = node
    let matches = findFirstMatch(text: text)
    if matches.count == 0 {
      return
    }

    for match in matches {
      let matchOffset = match.range.location
      let offset = textOffset + matchOffset
      let matchLength = match.range.length

      // Previous node is valid if any of:
      // 1. Space before same node
      // 2. Space in previous simple text node
      // 3. Previous node is LineBreakNode
      let contentBeforeMatchIsValid: Bool

      if offset > 0 {
        let index = nodeText.index(nodeText.startIndex, offsetBy: offset)
        let beforeIndex = nodeText.index(before: index)
        contentBeforeMatchIsValid = nodeText[beforeIndex..<index] == " "
      } else {
        contentBeforeMatchIsValid = isPreviousNodeValid(node: node)
      }

      // Next node is valid if any of:
      // 1. Space after same node
      // 2. Space in next simple text node
      // 3. Next node is LineBreakNode
      let contentAfterMatchIsValid: Bool

      if offset + matchLength < nodeTextLength {
        let index = nodeText.index(nodeText.startIndex, offsetBy: offset + matchLength)
        let afterIndex = nodeText.index(after: index)
        contentAfterMatchIsValid = nodeText[index..<afterIndex] == " "
      } else {
        contentAfterMatchIsValid = isNextNodeValid(node: node)
      }

      if contentAfterMatchIsValid && contentBeforeMatchIsValid {
        var middleNode: Node?

        if matchOffset == 0 {
          let nodes = try lastNode.splitText(splitOffsets: [matchLength])
          if nodes.count > 1 {
            middleNode = nodes[0]
            lastNode = nodes.count == 2 ? nodes[1] : lastNode
          }
        } else {
          let nodes = try lastNode.splitText(splitOffsets: [matchOffset, matchOffset + matchLength])
          if nodes.count >= 2 {
            // ignore the first node
            middleNode = nodes[1]
            lastNode = nodes.count == 3 ? nodes[2] : lastNode
          }
        }

        let linkNode = createAutoLinkNode(url: match.url)
        try linkNode.append([createTextNode(text: match.text)])
        try middleNode?.replace(replaceWith: linkNode)
      }

      textOffset += (matchOffset + matchLength)
    }
  }

  private func handleLinkEdit(linkNode: AutoLinkNode) throws {
    // Check children are simple text
    let children = linkNode.getChildren()

    for child in children {
      if !(child is TextNode) {
        try replaceWithChildren(node: linkNode)
        return
      }

      if let child = child as? TextNode, !child.isSimpleText() {
        try replaceWithChildren(node: linkNode)
        return
      }
    }

    // Check text content fully matches
    let text = linkNode.getTextContent()
    let matches = findFirstMatch(text: text)

    if matches.count == 0 || (matches.count >= 1 && matches[0].text != text) {
      try replaceWithChildren(node: linkNode)
      return
    }

    // Check neighbors
    if !isPreviousNodeValid(node: linkNode) || !isNextNodeValid(node: linkNode) {
      try replaceWithChildren(node: linkNode)
      return
    }

    let url = linkNode.getURL()

    if matches.count >= 1, matches[0].url != url {
      try linkNode.setURL(matches[0].url)
    }
  }

  // Bad neighbours are edits in neighbor nodes that make AutoLinks incompatible.
  // Given the creation preconditions, these can only be simple text nodes.
  private func handleBadNeighbors(textNode: TextNode) throws {
    let previousSibling = textNode.getPreviousSibling()
    let nextSibling = textNode.getNextSibling()
    let text = textNode.getTextContent()

    let startChar = String(text[text.startIndex])
    if let previousSibling = previousSibling as? AutoLinkNode, startChar != " " {
      try replaceWithChildren(node: previousSibling)
    }

    let endIndex = text.index(before: text.endIndex)
    let lastChar = String(text[endIndex])
    if let nextSibling = nextSibling as? AutoLinkNode, lastChar != " " {
      try replaceWithChildren(node: nextSibling)
    }
  }
}


/*
 * 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 Lexical
import LexicalLinkPlugin
import UIKit

extension NodeType {
  public static let autoLink = NodeType(rawValue: "autoLink")
}

public class AutoLinkNode: LinkNode {
  enum CodingKeys: String, CodingKey {
    case url
  }

  override public init() {
    super.init()
    self.type = NodeType.autoLink
  }

  required init(url: String, key: NodeKey?) {
    super.init(url: url, key: key)
    self.type = NodeType.autoLink
  }

  public required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    try super.init(from: decoder)

    self.url = try container.decode(String.self, forKey: .url)
    self.type = NodeType.autoLink
  }

  override open func encode(to encoder: Encoder) throws {
    try super.encode(to: encoder)
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(self.url, forKey: .url)
  }

  override open func clone() -> Self {
    Self(url: url, key: key)
  }

  override open func insertNewAfter(selection: RangeSelection?) throws -> Node? {
    if let element = try getParentOrThrow().insertNewAfter(selection: selection) as? ElementNode {
      let linkNode = AutoLinkNode(url: url, key: nil)
      try element.append([linkNode])
      return linkNode
    }

    return nil
  }
}