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

public enum TextNodeThemeSubtype {
  public static let code = "code"
}

public struct SerializedTextFormat: OptionSet, Codable {
  public let rawValue: Int

  public static let bold = SerializedTextFormat(rawValue: 1 << 0)
  public static let italic = SerializedTextFormat(rawValue: 1 << 1)
  public static let strikethrough = SerializedTextFormat(rawValue: 1 << 2)
  public static let underline = SerializedTextFormat(rawValue: 1 << 3)
  public static let code = SerializedTextFormat(rawValue: 1 << 4)
  public static let subScript = SerializedTextFormat(rawValue: 1 << 5)
  public static let superScript = SerializedTextFormat(rawValue: 1 << 6)

  public init(rawValue: Int) {
    self.rawValue = rawValue
  }

  // On encode, convert from TextFormat -> SerializedTextFormat
  public static func convertToSerializedTextFormat(from textFormat: TextFormat) -> SerializedTextFormat {
    var serialTextFormat = SerializedTextFormat()
    if textFormat.bold {
      serialTextFormat.insert(.bold)
    }
    if textFormat.italic {
      serialTextFormat.insert(.italic)
    }
    if textFormat.underline {
      serialTextFormat.insert(.underline)
    }
    if textFormat.strikethrough {
      serialTextFormat.insert(.strikethrough)
    }
    if textFormat.code {
      serialTextFormat.insert(.code)
    }
    if textFormat.subScript {
      serialTextFormat.insert(.subScript)
    }
    if textFormat.superScript {
      serialTextFormat.insert(.superScript)
    }

    return serialTextFormat
  }

  // On decode, convert from SerializedTextFormat -> TextFormat
  public static func convertToTextFormat(from serialTextFormat: SerializedTextFormat) -> TextFormat {
    var textFormat = TextFormat()
    if serialTextFormat.contains(.bold) {
      textFormat.bold = true
    }
    if serialTextFormat.contains(.italic) {
      textFormat.italic = true
    }
    if serialTextFormat.contains(.underline) {
      textFormat.underline = true
    }
    if serialTextFormat.contains(.strikethrough) {
      textFormat.strikethrough = true
    }
    if serialTextFormat.contains(.code) {
      textFormat.code = true
    }
    if serialTextFormat.contains(.subScript) {
      textFormat.subScript = true
    }
    if serialTextFormat.contains(.superScript) {
      textFormat.superScript = true
    }

    return textFormat
  }
}

public struct TextFormat: Equatable, Codable {

  public var bold: Bool
  public var italic: Bool
  public var underline: Bool
  public var strikethrough: Bool
  public var code: Bool
  public var subScript: Bool
  public var superScript: Bool

  public init() {
    self.bold = false
    self.italic = false
    self.underline = false
    self.strikethrough = false
    self.code = false
    self.subScript = false
    self.superScript = false
  }

  func isTypeSet(type: TextFormatType) -> Bool {
    switch type {
    case .bold:
      return bold
    case .italic:
      return italic
    case .underline:
      return underline
    case .strikethrough:
      return strikethrough
    case .code:
      return code
    case .subScript:
      return subScript
    case .superScript:
      return superScript
    }
  }

  public mutating func updateFormat(type: TextFormatType, value: Bool) {
    switch type {
    case .bold:
      bold = value
    case .italic:
      italic = value
    case .underline:
      underline = value
    case .strikethrough:
      strikethrough = value
    case .code:
      code = value
    case .subScript:
      subScript = value
    case .superScript:
      superScript = value
    }
  }
}

struct SerializedTextNodeDetail: OptionSet, Codable {
  public let rawValue: Int

  public static let isDirectionless = SerializedTextNodeDetail(rawValue: 1 << 0)
  public static let isUnmergeable = SerializedTextNodeDetail(rawValue: 1 << 1)

  public init(rawValue: Int) {
    self.rawValue = rawValue
  }

  // On encode, convert from TextNodeDetail -> SerializedTextNodeDetail
  public static func convertToSerializedTextNodeDetail(from textDetail: TextNodeDetail) -> SerializedTextNodeDetail {
    var serialTextDetail = SerializedTextNodeDetail()
    if textDetail.isDirectionless {
      serialTextDetail.insert(.isDirectionless)
    }
    if textDetail.isUnmergable {
      serialTextDetail.insert(.isUnmergeable)
    }
    return serialTextDetail
  }

  // On decode, convert from SerializedTextNodeDetail -> TextNodeDetail
  public static func convertToTextDetail(from serialTextDetail: SerializedTextNodeDetail) -> TextNodeDetail {
    var textDetail = TextNodeDetail()
    if serialTextDetail.contains(.isDirectionless) {
      textDetail.isDirectionless = true
    }
    if serialTextDetail.contains(.isUnmergeable) {
      textDetail.isUnmergable = true
    }

    return textDetail
  }
}

struct TextNodeDetail: Codable {
  var isDirectionless: Bool = false
  var isUnmergable: Bool = false
}

open class TextNode: Node {
  enum CodingKeys: String, CodingKey {
    case text
    case mode
    case format
    case detail
    case style
  }

  private var text: String = ""
  var mode: Mode = .normal
  var format: TextFormat = TextFormat()
  var detail = TextNodeDetail()
  var style: String = ""

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

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

  public convenience init(text: String) {
    self.init(text: text, key: LexicalConstants.uninitializedNodeKey)
  }

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

    self.text = try container.decode(String.self, forKey: .text)
    self.mode = try container.decode(Mode.self, forKey: .mode)
    let serializedFormat = try container.decode(SerializedTextFormat.self, forKey: .format)
    self.format = SerializedTextFormat.convertToTextFormat(from: serializedFormat)
    let serializedDetail = try container.decode(SerializedTextNodeDetail.self, forKey: .detail)
    self.detail = SerializedTextNodeDetail.convertToTextDetail(from: serializedDetail)
    self.style = try container.decode(String.self, forKey: .style)
    self.type = NodeType.text
  }

  override open func encode(to encoder: Encoder) throws {
    try super.encode(to: encoder)
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(self.text, forKey: .text)
    try container.encode(self.mode, forKey: .mode)
    try container.encode(SerializedTextFormat.convertToSerializedTextFormat(from: self.format).rawValue, forKey: .format)
    try container.encode(SerializedTextNodeDetail.convertToSerializedTextNodeDetail(from: self.detail).rawValue, forKey: .detail)
    try container.encode(self.style, forKey: .style)
  }

  override public func getTextPart() -> String {
    return getLatest().text
  }

  public func setText(_ text: String) throws {
    try errorOnReadOnly()
    try getWritable().text = text
  }

  public func setText_dangerousPropertyAccess(_ text: String) {
    self.text = text
  }

  public func getText_dangerousPropertyAccess() -> String {
    return self.text
  }

  public func setBold(_ isBold: Bool) throws {
    try errorOnReadOnly()
    try getWritable().format.bold = isBold
  }

  public func setItalic(_ isItalic: Bool) throws {
    try errorOnReadOnly()
    try getWritable().format.italic = isItalic
  }

  public func canInsertTextAfter() -> Bool {
    return true
  }

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

  override open func getAttributedStringAttributes(theme: Theme) -> [NSAttributedString.Key: Any] {
    var attributeDictionary = super.getAttributedStringAttributes(theme: theme)

    // TODO: Remove this once codeHighlight node is implemented
    if let parent, let _ = getNodeByKey(key: parent) as? CodeNode {
      format = TextFormat()
    }

    if format.bold {
      attributeDictionary[.bold] = true
    }

    if format.italic {
      attributeDictionary[.italic] = true
    }

    if format.underline {
      attributeDictionary[.underlineStyle] = NSUnderlineStyle.single.rawValue
    }

    if format.strikethrough {
      attributeDictionary[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
    }

    if format.code {
      if let themeDict = theme.getValue(.text, withSubtype: TextNodeThemeSubtype.code) {
        attributeDictionary.merge(themeDict) { (_, new) in new }
      } else {
        attributeDictionary[NSAttributedString.Key.fontFamily] = "Courier"
        attributeDictionary[NSAttributedString.Key.backgroundColor] = UIColor.lightGray
      }
    }

    return attributeDictionary
  }

  public func isInert() -> Bool {
    let node = getLatest() as TextNode
    return node.mode == .inert
  }

  public func isToken() -> Bool {
    let node = getLatest() as TextNode
    return node.mode == .token
  }

  public func isComposing() -> Bool {
    return key == getCompositionKey()
  }

  public func spliceText(
    offset: Int,
    delCount: Int,
    newText: String,
    moveSelection: Bool = false
  ) throws -> TextNode {
    try errorOnReadOnly()
    let writableNode = try getWritable() as TextNode
    let text = writableNode.getTextPart()
    var index = offset

    if index < 0 {
      index = newText.lengthAsNSString() + index
      if index < 0 {
        index = 0
      }
    }

    let convertedText = text as NSString
    let prefixText = convertedText.substring(
      with: NSRange(
        location: 0,
        length: index
      )
    )

    // In JS, slice will continue to function even if there are bad boundary conditions. Here,
    // we clamp with substring to permit a similar resilience to maintain parity with web code.
    let postText = convertedText.substring(
      with: NSRange(
        location: max(min(index + delCount, convertedText.length), 0),
        length: max(min(convertedText.length - (index + delCount), convertedText.length), 0)
      )
    )

    try writableNode.setText("\(prefixText)\(newText)\(postText)")

    let selection = try getSelection(allowInvalidPositions: true)
    if moveSelection, let selection = selection as? RangeSelection {
      let newOffset = offset + newText.lengthAsNSString()
      selection.setTextNodeRange(
        anchorNode: writableNode,
        anchorOffset: newOffset,
        focusNode: writableNode,
        focusOffset: newOffset
      )
    }

    return writableNode
  }

  public func isSegmented() -> Bool {
    let node = getLatest() as TextNode
    return node.mode == .segmented
  }

  @discardableResult
  public func setMode(mode: Mode) throws -> TextNode {
    try errorOnReadOnly()
    let node = try getWritable() as TextNode
    node.mode = mode
    return getLatest()
  }

  func canInsertTextBefore() -> Bool {
    return true
  }

  public func getFormat() -> TextFormat {
    let node = getLatest() as TextNode
    return node.format
  }

  @discardableResult
  public func setFormat(format: TextFormat) throws -> TextNode {
    try errorOnReadOnly()
    let node = try getWritable() as TextNode
    node.format = format
    return node
  }

  public func getStyle() -> String {
    let node = getLatest() as TextNode
    return node.style
  }

  public func splitText(splitOffsets: [Int]) throws -> [TextNode] {
    try errorOnReadOnly()
    let textContent = getTextPart() as NSString
    let textLength = textContent.length
    let offsetsSet = Set(splitOffsets)
    var parts = [String]()
    var string: NSMutableString = NSMutableString(string: "")

    for i in 0..<textLength {
      if string != "" && offsetsSet.contains(i) {
        parts.append(string as String)
        string = NSMutableString(string: "")
      }
      string.append(textContent.substring(with: NSRange(location: i, length: 1)))
    }
    if string != "" {
      parts.append(string as String)
    }
    let partsLength = parts.count
    if partsLength == 0 {
      return []
    } else if parts[0] == textContent as String {
      return [getLatest()]
    }
    let firstPart = parts[0]
    guard let parent = getParent() else {
      return []
    }
    let parentKey = parent.key
    var writableNode = TextNode()
    let format = getFormat()
    let style = getStyle()
    let detail = detail
    var hasReplacedSelf = false

    if isSegmented() {
      // Create a new TextNode
      writableNode = createTextNode(text: firstPart)
      writableNode.parent = parentKey
      writableNode.format = format
      writableNode.style = style
      writableNode.detail = detail
      hasReplacedSelf = true
    } else {
      // For the first part, update the existing node
      writableNode = try getWritable()
      writableNode.text = firstPart
    }
    // Handle selection
    let selection = try getSelection()

    // Then handle all other parts
    var splitNodes = [writableNode]
    var textSize = firstPart.lengthAsNSString()
    for i in 1..<partsLength {
      let part = parts[i]
      let partSize = part.lengthAsNSString()
      let sibling = try createTextNode(text: part).getWritable()
      sibling.format = format
      sibling.style = style
      sibling.detail = detail
      let siblingKey = sibling.key
      let nextTextSize = textSize + partSize

      if let selection = selection as? RangeSelection {
        let anchor = selection.anchor
        let focus = selection.focus

        if anchor.key == key &&
            anchor.type == .text &&
            anchor.offset > textSize &&
            anchor.offset <= nextTextSize {
          anchor.key = siblingKey
          anchor.offset -= textSize
          selection.dirty = true
        }

        if focus.key == key &&
            focus.type == .text &&
            focus.offset > textSize &&
            focus.offset <= nextTextSize {
          focus.key = siblingKey
          focus.offset -= textSize
          selection.dirty = true
        }
      }

      textSize = nextTextSize
      sibling.parent = parentKey
      splitNodes.append(sibling)
    }
    // Insert the nodes into the parent's children
    internallyMarkNodeAsDirty(node: self, cause: .userInitiated)
    let writableParent = try parent.getWritable() as ElementNode
    guard let insertionIndex = writableParent.children.firstIndex(of: key) else { return [] }

    let splitNodesKeys = splitNodes.map({ splitNode in
      splitNode.key
    })

    if hasReplacedSelf {
      writableParent.children.insert(contentsOf: splitNodesKeys, at: insertionIndex)
      try remove()
    } else {
      writableParent.children.replaceSubrange(insertionIndex...insertionIndex, with: splitNodesKeys)
    }

    if let selection = selection as? RangeSelection {
      try updateElementSelectionOnCreateDeleteNode(
        selection: selection,
        parentNode: parent,
        nodeOffset: insertionIndex,
        times: partsLength - 1)
    }
    return splitNodes
  }

  public func isSimpleText() -> Bool {
    return type == NodeType.text && mode == .normal
  }

  @discardableResult
  public func select(anchorOffset: Int?, focusOffset: Int?) throws -> RangeSelection {
    try errorOnReadOnly()
    let selection = try getSelection()
    let text = getTextPart()

    let lastOffset = text.lengthAsNSString()
    var updatedAnchorOffset = lastOffset
    var updatedFocusOffset = lastOffset
    if let anchorOffset {
      updatedAnchorOffset = anchorOffset
    }
    if let focusOffset {
      updatedFocusOffset = focusOffset
    }

    guard let selection else {
      return try makeRangeSelection(
        anchorKey: key,
        anchorOffset: updatedAnchorOffset,
        focusKey: key,
        focusOffset: updatedFocusOffset,
        anchorType: .text,
        focusType: .text)
    }
    guard let selection = selection as? RangeSelection else {
      return try makeRangeSelection(anchorKey: key, anchorOffset: anchorOffset ?? 0, focusKey: key, focusOffset: focusOffset ?? 0, anchorType: .text, focusType: .text)
    }

    selection.setTextNodeRange(anchorNode: self, anchorOffset: updatedAnchorOffset, focusNode: self, focusOffset: updatedFocusOffset)

    return selection
  }

  public func getFormatFlags(type: TextFormatType, alignWithFormat: TextFormat? = nil) -> TextFormat {
    let node = getLatest() as TextNode
    let format = node.format
    return toggleTextFormatType(format: format, type: type, alignWithFormat: alignWithFormat)
  }

  public func mergeWithSibling(target: TextNode) throws -> TextNode {
    var isBefore: Bool
    isBefore = target == getPreviousSibling()

    if !isBefore && target != getNextSibling() {
      throw LexicalError.internal("mergeWithSibling: sibling must be a previous or next sibling")
    }

    let targetKey = target.key
    let textLength = text.lengthAsNSString()
    let selection = try getSelection()
    if let selection = selection as? RangeSelection {
      let anchor = selection.anchor
      let focus = selection.focus

      for point in [anchor, focus] {
        if point.key == targetKey {
          // The Point was inside the now removed node
          adjustPointOffsetForMergedSibling(point: point,
                                            isBefore: isBefore,
                                            key: key,
                                            target: target,
                                            textLength: textLength)
          selection.dirty = true
        } else if point.key == self.key && point.type == .text && isBefore {
          // The Point is in self, and it's type text, and we're being merged with a previous sibling.
          // So we need to adjust the point's offset to accommodate.
          point.offset += target.getTextPartSize()
          selection.dirty = true
        }
      }
    }
    let newText = isBefore ? target.text + text : text + target.text
    try setText(newText)
    try target.remove()
    return getLatest()
  }

  override public func getTextContent(
    includeInert: Bool = false,
    includeDirectionless: Bool = false
  ) -> String {
    if (!includeInert && isInert()) || (!includeDirectionless && isDirectionless()) {
      return ""
    }

    let node = getLatest() as TextNode
    return node.getTextPart()
  }

  @discardableResult
  public func toggleDirectionless() throws -> TextNode {
    try errorOnReadOnly()
    let node = try getWritable() as TextNode
    node.detail.isDirectionless = !node.detail.isDirectionless
    return getLatest()
  }

  func isDirectionless() -> Bool {
    let node = getLatest() as TextNode
    return node.detail.isDirectionless
  }

  func isUnmergeable() -> Bool {
    let node = getLatest() as TextNode
    return node.detail.isUnmergable
  }

  static func canSimpleTextNodesBeMerged(node1: TextNode, node2: TextNode) -> Bool {
    let node1Mode = node1.mode
    let node1Format = node1.format
    let node1Style = node1.style
    let node2Mode = node2.mode
    let node2Format = node2.format
    let node2Style = node2.style

    return node1Mode == node2Mode &&
      node1Format == node2Format &&
      node1Style == node2Style
  }

  static func mergeTextNodes(node1: TextNode, node2: TextNode) throws -> TextNode {
    let writableNode1 = try node1.mergeWithSibling(target: node2)
    guard let editor = getActiveEditor() else {
      throw LexicalError.invariantViolation("Cannot be called outside update loop")
    }
    var normalizedNodes = editor.normalizedNodes
    normalizedNodes.insert(node1.key)
    normalizedNodes.insert(node2.key)
    return writableNode1
  }

  static func normalizeTextNode(textNode: TextNode) throws {
    var node = textNode
    if node.text == "" && node.isSimpleText() && !node.isUnmergeable() {
      try node.remove()
      return
    }
    // Backward
    while let previousNode = node.getPreviousSibling() {
      guard let textNode = previousNode as? TextNode, textNode.isSimpleText(), !textNode.isUnmergeable() else { break }

      if textNode.text == "" {
        try textNode.remove()
      } else if canSimpleTextNodesBeMerged(node1: textNode, node2: node) {
        node = try mergeTextNodes(node1: textNode, node2: node)
        break
      } else {
        break
      }
    }
    // Forward
    while let nextNode = node.getNextSibling() {
      guard let textNode = nextNode as? TextNode, textNode.isSimpleText(), !textNode.isUnmergeable() else { break }

      if textNode.text == "" {
        try textNode.remove()
      } else if canSimpleTextNodesBeMerged(node1: node, node2: textNode) {
        node = try mergeTextNodes(node1: node, node2: textNode)
        break
      } else {
        break
      }
    }
  }

  public static func ==(lhs: TextNode, rhs: TextNode) -> Bool {
    return (lhs.getTextPart() == rhs.getTextPart()) && (lhs.key == rhs.key)
  }
}

extension TextNode: CustomDebugStringConvertible {
  public var debugDescription: String {
    return "TextNode: \"\(text)\""
  }
}

extension TextFormat: CustomDebugStringConvertible {
  public var debugDescription: String {
    var debugStyleStatus = [String]()
    if bold { debugStyleStatus.append("bold") }
    if italic { debugStyleStatus.append("italic") }
    if underline { debugStyleStatus.append("underline") }
    if strikethrough { debugStyleStatus.append("strikeThrough") }
    if code { debugStyleStatus.append("code") }
    return debugStyleStatus.joined(separator: ", ")
  }
}

public extension NSAttributedString.Key {
  static let inlineCodeBackgroundColor: NSAttributedString.Key = .init(rawValue: "inlineCodeBackgroundColor")
}

extension TextNode {
  internal static var inlineCodeBackgroundDrawing: CustomDrawingHandler {
    get {
      return { attributeKey, attributeValue, layoutManager, attributeRunCharacterRange, granularityExpandedCharacterRange, glyphRange, rect, firstLineFragment in
        guard let attributeValue = attributeValue as? UIColor else { return }
        attributeValue.setFill()
        UIRectFill(rect)
      }
    }
  }
}


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

// NB: This class is meant to represent unknown nodes serialized *with JSON*. This is a very
//     important distinction as the class has no means of representing mappings with keys as
//     arbitrary values (ex: say { null: new Uint8Array() } were encoded with a protobuf)
//     which is possible with the generic Codable interface.
public class UnknownNode: Node {
  struct AnyCodingKey: CodingKey {
    var stringValue: String
    var intValue: Int?

    init?(stringValue: String) {
      self.stringValue = stringValue
    }

    init?(intValue: Int) {
      return nil
    }
  }

  enum SupportedValue: Codable, Equatable {
    case null
    case number(Float)
    case bool(Bool)
    case string(String)
    case object([String: SupportedValue])
    case array([SupportedValue])

    func encode(to encoder: Encoder) throws {
      // Unfortunately in Swift there is no way to "defer" serialization. Since this is intended to
      // be used with JSON primitives we support what JSON supports out of the box.
      var container = encoder.singleValueContainer()

      switch self {
      case .null:
        try container.encodeNil()
      case .bool(let value):
        try container.encode(value)
      case .number(let value):
        try container.encode(value)
      case .string(let value):
        try container.encode(value)
      case .object(let values):
        try container.encode(values)
      case .array(let values):
        try container.encode(values)
      }
    }

    init(from decoder: Decoder) throws {
      if let container = try? decoder.container(keyedBy: AnyCodingKey.self) {
        var values = [String: SupportedValue]()

        for codingKey in container.allKeys {
          // According to the JSON spec, any valid key must be a double-quoted string. Therefore,
          // we assume keys are strings.
          values[codingKey.stringValue] = try SupportedValue(from: container.superDecoder(forKey: codingKey))
        }

        self = .object(values)
      } else if var container = try? decoder.unkeyedContainer() {
        var values = [SupportedValue]()

        if let count = container.count {
          values.reserveCapacity(count)
        }

        while !container.isAtEnd {
          values.append(try container.decode(SupportedValue.self))
        }

        self = .array(values)
      } else if let container = try? decoder.singleValueContainer() {
        if let value = try? container.decode(Bool.self) {
          self = .bool(value)
        } else if let value = try? container.decode(Float.self) {
          self = .number(value)
        } else if let value = try? container.decode(Int.self) {
          self = .number(Float(value))
        } else if let value = try? container.decode(String.self) {
          self = .string(value)
        } else if container.decodeNil() {
          self = .null
        } else {
          throw LexicalError.invariantViolation("Unsupported value present in decoded node map")
        }
      } else {
        throw LexicalError.invariantViolation("Unsupported value present in decoded node map")
      }
    }
  }

  private(set) var data: SupportedValue = .null

  override var parent: NodeKey? {
    didSet {
      if case var .object(values) = data {
        if let value = parent {
          values["parent"] = .string(value)
        } else {
          values.removeValue(forKey: "parent")
        }
      }
    }
  }

  public required init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()

    data = try container.decode(SupportedValue.self)

    typealias Keys = Node.CodingKeys

    // NB: As consuming keys in a coding container is a stateful operation, certain keys are
    //     re-extracted and initialized similarly to the super class here.
    super.init()

    if case let .object(values) = data {

      if case let .string(type) = values[Keys.type.rawValue] {
        self.type = NodeType(rawValue: type)
      } else {
        throw LexicalError.invariantViolation("No type passed to UnknownNode")
      }
    }
  }

  override open func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()

    try container.encode(data)
  }

  override public func getTextPart() -> String {
    "â–¡"
  }
}


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

import Foundation

public protocol BaseSelection: AnyObject, CustomDebugStringConvertible {
  var dirty: Bool { get set }
  func clone() -> BaseSelection
  func extract() throws -> [Node]
  func getNodes() throws -> [Node]
  func getTextContent() throws -> String
  func insertRawText(_ text: String) throws
  func isSelection(_ selection: BaseSelection) -> Bool
}


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

import Foundation
import UIKit

public class GridSelection: BaseSelection {
  public func getTextContent() throws -> String {
    return ""
  }

  public func insertRawText(_ text: String) {
    // no-op
  }

  public func isSelection(_ selection: BaseSelection) -> Bool {
    return false // TODO
  }

  public var gridKey: NodeKey
  public var anchorCellKey: NodeKey
  public var focusCellKey: NodeKey
  public var dirty: Bool = false

  // MARK: - Init

  public init(gridKey: NodeKey, anchorCellKey: NodeKey, focusCellKey: NodeKey) {
    self.gridKey = gridKey
    self.anchorCellKey = anchorCellKey
    self.focusCellKey = focusCellKey
  }

  public func clone() -> BaseSelection {
    return GridSelection(gridKey: gridKey, anchorCellKey: anchorCellKey, focusCellKey: focusCellKey)
  }

  public func getNodes() throws -> [Node] {
    return []
  }

  public func extract() throws -> [Node] {
    return []
  }
}

extension GridSelection: CustomDebugStringConvertible {
  public var debugDescription: String {
    return "Grid Selection"
  }
}


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

import Foundation
import UIKit

public class NodeSelection: BaseSelection {

  public var nodes: Set<NodeKey>
  public var dirty: Bool = false

  public init(nodes: Set<NodeKey>) {
    self.nodes = nodes
  }

  public func clone() -> BaseSelection {
    return NodeSelection(nodes: nodes)
  }

  public func add(key: NodeKey) {
    dirty = true
    nodes.insert(key)
  }

  public func delete(key: NodeKey) {
    dirty = true
    nodes.remove(key)
  }

  public func clear() {
    dirty = true
    nodes.removeAll()
  }

  public func has(key: NodeKey) -> Bool {
    return nodes.contains(key)
  }

  public func getNodes() throws -> [Node] {
    let objects = self.nodes
    var nodesToReturn: [Node] = []
    for object in objects {
      if let node = getNodeByKey(key: object) {
        nodesToReturn.append(node)
      }
    }
    return nodesToReturn
  }

  public func extract() throws -> [Node] {
    return try getNodes()
  }

  public func getTextContent() throws -> String {
    let nodes = try getNodes()
    var textContent = ""
    for node in nodes {
      textContent.append(node.getTextContent())
    }
    return textContent
  }

  public func insertRawText(_ text: String) {
    // do nothing
  }

  public func isSelection(_ selection: BaseSelection) -> Bool {
    guard let selection = selection as? NodeSelection else {
      return false
    }
    return nodes == selection.nodes
  }
}

extension NodeSelection: CustomDebugStringConvertible {
  public var debugDescription: String {
    return "Node Selection"
  }
}


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

import Foundation

public enum SelectionType: String {
  case text = "text"
  case element = "element"
  case range = "range"
  case node = "node"
  case grid = "grid"
}

public class Point {
  public var key: NodeKey
  public var offset: Int
  public var type: SelectionType
  internal weak var selection: BaseSelection?

  public init(key: NodeKey, offset: Int, type: SelectionType) {
    self.key = key
    self.offset = offset
    self.type = type
    self.selection = nil
  }

  func isBefore(point b: Point) throws -> Bool {
    var aNode = try getNode()
    var bNode = try b.getNode()
    let aOffset = offset
    let bOffset = b.offset

    if let elementNode = aNode as? ElementNode {
      aNode = elementNode.getDescendantByIndex(index: aOffset) ?? aNode
    }

    if let elementNode = bNode as? ElementNode {
      bNode = elementNode.getDescendantByIndex(index: bOffset) ?? bNode
    }

    if aNode == bNode {
      return aOffset < bOffset
    }

    return aNode.isBefore(bNode)
  }

  public func getNode() throws -> Node {
    guard let node = getNodeByKey(key: key) else {
      throw LexicalError.internal("Point.getNode: node not found")
    }

    return node
  }

  public func getOffset() -> Int {
    return offset
  }

  public func getType() -> SelectionType {
    return type
  }

  public func updatePoint(key: NodeKey, offset: Int, type: SelectionType) {
    self.key = key
    self.offset = offset
    self.type = type

    if !isReadOnlyMode() {
      if let selection {
        selection.dirty = true
      }
    }
  }

  func getCharacterOffset() -> Int {
    type == .text ? offset : 0
  }

  public func isAtNodeEnd() throws -> Bool {
    switch type {
    case .element:
      if let elementNode = try getNode() as? ElementNode {
        return offset == elementNode.children.count
      }

    case .text:
      if let textNode = try getNode() as? TextNode {
        return offset == textNode.getTextPart().lengthAsNSString()
      }
    case .range:
      throw LexicalError.invariantViolation("Need range selection")
    case .node:
      throw LexicalError.invariantViolation("Need node selection")
    case .grid:
      throw LexicalError.invariantViolation("Need grid selection")
    }

    return false
  }
}

extension Point: Equatable {
  public static func == (lhs: Point, rhs: Point) -> Bool {
    return lhs.key == rhs.key &&
      lhs.offset == rhs.offset &&
      lhs.type == rhs.type
  }
}

extension Point: CustomDebugStringConvertible {
  public var debugDescription: String {
    return "key: \(key), offset: \(offset), type: \(type) }"
  }
}


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

import Foundation
import UIKit

public class RangeSelection: BaseSelection {

  public var anchor: Point
  public var focus: Point
  public var dirty: Bool
  public var format: TextFormat

  // MARK: - Init

  public init(anchor: Point, focus: Point, format: TextFormat) {
    self.anchor = anchor
    self.focus = focus
    self.dirty = false
    self.format = format

    anchor.selection = self
    focus.selection = self
  }

  // MARK: - Public

  public func isBackward() throws -> Bool {
    return try focus.isBefore(point: anchor)
  }

  public func isCollapsed() -> Bool {
    return anchor == focus
  }

  public func hasFormat(type: TextFormatType) -> Bool {
    return format.isTypeSet(type: type)
  }

  public func getCharacterOffsets(selection: RangeSelection) -> (Int, Int) {
    let anchor = selection.anchor
    let focus = selection.focus
    if anchor.type == .element && focus.type == .element &&
        anchor.key == focus.key && anchor.offset == focus.offset {
      return (0, 0)
    }
    return (anchor.getCharacterOffset(), focus.getCharacterOffset())
  }

  public func getNodes() throws -> [Node] {
    let isBefore = try anchor.isBefore(point: focus)
    let firstPoint = isBefore ? anchor : focus
    let lastPoint = isBefore ? focus : anchor
    var firstNode = try firstPoint.getNode()
    var lastNode = try lastPoint.getNode()
    let startOffset = firstPoint.offset
    let endOffset = lastPoint.offset

    if let elementNode = firstNode as? ElementNode, let descendent = elementNode.getDescendantByIndex(index: startOffset) {
      firstNode = descendent
    }
    if let elementNode = lastNode as? ElementNode {
      var lastNodeDescendant = elementNode.getDescendantByIndex(index: endOffset)
      // We don't want to over-select, as node selection infers the child before
      // the last descendant, not including that descendant.
      if let lastNodeDescendantUnwrapped = lastNodeDescendant,
         lastNodeDescendantUnwrapped != firstNode,
         elementNode.getChildAtIndex(index: endOffset) == lastNodeDescendant {
        lastNodeDescendant = lastNodeDescendantUnwrapped.getPreviousSibling()
      }
      lastNode = lastNodeDescendant ?? lastNode
    }
    if firstNode == lastNode {
      if let firstNode = firstNode as? ElementNode, firstNode.getChildrenSize() > 0 {
        return []
      }
      return [firstNode]
    }
    return firstNode.getNodesBetween(targetNode: lastNode)
  }

  public func clone() -> BaseSelection {
    let selectionAnchor = createPoint(key: anchor.key,
                                      offset: anchor.offset,
                                      type: anchor.type)
    let selectionFocus = createPoint(key: focus.key,
                                     offset: focus.offset,
                                     type: focus.type)
    return RangeSelection(anchor: selectionAnchor, focus: selectionFocus, format: format)
  }

  public func setTextNodeRange(anchorNode: TextNode,
                               anchorOffset: Int,
                               focusNode: TextNode,
                               focusOffset: Int) {
    anchor.updatePoint(key: anchorNode.key, offset: anchorOffset, type: .text)
    focus.updatePoint(key: focusNode.key, offset: focusOffset, type: .text)
    dirty = true
  }

  public func extract() throws -> [Node] {
    var selectedNodes = try getNodes()
    guard let firstNode = selectedNodes.first else { return [] }

    let lastNode = selectedNodes.last
    let anchorOffset = anchor.getCharacterOffset()
    let focusOffset = focus.getCharacterOffset()

    if selectedNodes.isEmpty {
      return []
    } else if selectedNodes.count == 1 {
      guard let firstNode = firstNode as? TextNode else { return [firstNode] }

      let startOffset = anchorOffset > focusOffset ? focusOffset : anchorOffset
      let endOffset = anchorOffset > focusOffset ? anchorOffset : focusOffset
      let splitNodes = try firstNode.splitText(splitOffsets: [startOffset, endOffset])
      guard let node = startOffset == 0 ? splitNodes.first : splitNodes[1] else { return [] }
      return [node]
    }

    let isBefore = try anchor.isBefore(point: focus)

    if let firstNode = firstNode as? TextNode {
      let startOffset = isBefore ? anchorOffset : focusOffset
      if startOffset == firstNode.getTextContentSize() {
        selectedNodes.removeFirst()
      } else if startOffset != 0 {
        let splitNodes = try firstNode.splitText(splitOffsets: [startOffset])
        selectedNodes[0] = splitNodes[0]
      }
    }

    if let lastNode = lastNode as? TextNode {
      let lastNodeText = lastNode.getTextContent()
      let endOffset = isBefore ? focusOffset : anchorOffset

      if endOffset == 0 {
        selectedNodes.removeLast()
      } else if endOffset != lastNodeText.lengthAsNSString() {
        let splitNodes = try lastNode.splitText(splitOffsets: [endOffset])
        selectedNodes.append(splitNodes[0])
      }
    }

    return selectedNodes
  }

  public func insertRawText(text: String) throws {
    let parts = text.split { $0 == "\n" || $0 == "\r\n" }.map(String.init)
    if parts.count == 1 {
      try insertText(text)
    } else {
      var nodes = [Node]()
      let length = parts.count
      for (index, _) in parts.enumerated() {
        let part = parts[index]
        if !part.isEmpty {
          nodes.append(createTextNode(text: part))
        }
        if index != length - 1 {
          nodes.append(createLineBreakNode())
        }
      }
      _ = try insertNodes(nodes: nodes, selectStart: false)
    }
  }

  public func getTextContent() throws -> String {
    let nodes = try getNodes()
    if nodes.count == 0 {
      return ""
    }
    let firstNode = nodes[0]
    let lastNode = nodes[nodes.count - 1]
    let isBefore = try anchor.isBefore(point: focus)
    let anchorOffset = anchor.getCharacterOffset()
    let focusOffset = focus.getCharacterOffset()
    var textContent = ""
    var prevWasElement = true
    var textSliceAnchor = String()
    var textSliceFocus = String()

    for node in nodes {
      if let elementNode = node as? ElementNode, !elementNode.isInline() {
        if !prevWasElement {
          textContent += "\n"
        }
        if elementNode.isEmpty() {
          prevWasElement = false
        } else {
          prevWasElement = true
        }
      } else {
        prevWasElement = false
        if isTextNode(node) {
          var text = node.getTextContent()

          if node == firstNode {
            if node == lastNode {
              if anchorOffset < focusOffset {
                let anchorRange = NSRange(location: anchorOffset, length: focusOffset - anchorOffset)
                textSliceAnchor = (text as NSString).substring(with: anchorRange)
              } else {
                let focusRange = NSRange(location: focusOffset, length: anchorOffset - focusOffset)
                textSliceFocus = (text as NSString).substring(with: focusRange)
              }

              text = anchorOffset < focusOffset ? textSliceAnchor : textSliceFocus
            } else {
              if isBefore {
                textSliceAnchor = (text as NSString).substring(from: anchorOffset)
              } else {
                textSliceFocus = (text as NSString).substring(from: focusOffset)
              }

              text = isBefore ? textSliceAnchor : textSliceFocus
            }
          } else if node == lastNode {
            if isBefore {
              textSliceAnchor = (text as NSString).substring(to: focusOffset)
            } else {
              textSliceFocus = (text as NSString).substring(to: anchorOffset)
            }

            text = isBefore ? textSliceAnchor : textSliceFocus
          }

          textContent += text
        } else if (isDecoratorNode(node) || isLineBreakNode(node)) && (node != lastNode || !isCollapsed()) {
          textContent += node.getTextContent()
        }
      }
    }

    return textContent
  }

  public func insertText(_ text: String) throws {
    let anchor = anchor
    let focus = focus
    let isBefore = try isCollapsed() || anchor.isBefore(point: focus)

    if isBefore && anchor.type == .element {
      try transferStartingElementPointToTextPoint(start: anchor, end: focus, format: format)
    } else if !isBefore && focus.type == .element {
      try transferStartingElementPointToTextPoint(start: focus, end: anchor, format: format)
    }

    let selectedNodes = try getNodes()
    let selectedNodesLength = selectedNodes.count
    let firstPoint = isBefore ? anchor : focus
    let endPoint = isBefore ? focus : anchor
    let startOffset = firstPoint.offset
    let endOffset = endPoint.offset

    guard var firstNode = selectedNodes[0] as? TextNode else {
      throw LexicalError.invariantViolation("insertText: first node is not a text node")
    }

    let firstNodeText = firstNode.getTextPart()
    let firstNodeTextLength = firstNodeText.lengthAsNSString()
    let firstNodeParent = try firstNode.getParentOrThrow()

    if isCollapsed() && startOffset == firstNodeTextLength &&
        (firstNode.isSegmented() || firstNode.isToken() || !firstNode.canInsertTextAfter() || !firstNodeParent.canInsertTextAfter()) {
      var nextSibling = firstNode.getNextSibling() as? TextNode
      if !isTextNode(nextSibling) || isTokenOrInertOrSegmented(nextSibling) {
        nextSibling = createTextNode(text: "")
        guard let nextSibling else {
          return
        }
        if !firstNodeParent.canInsertTextAfter() {
          _ = try firstNodeParent.insertAfter(nodeToInsert: nextSibling)
        } else {
          _ = try firstNode.insertAfter(nodeToInsert: nextSibling)
        }
      }
      guard let nextSibling else {
        return
      }
      _ = try nextSibling.select(anchorOffset: 0, focusOffset: 0)
      firstNode = nextSibling
      if text != "" {
        try insertText(text)
        return
      }
    } else if isCollapsed() && startOffset == 0 &&
                (firstNode.isSegmented() || firstNode.isToken() || !firstNode.canInsertTextBefore() || !firstNodeParent.canInsertTextBefore()) {
      var prevSibling = firstNode.getPreviousSibling() as? TextNode
      if !isTextNode(prevSibling ) || isTokenOrInertOrSegmented(prevSibling) {
        prevSibling = createTextNode(text: "")
        guard let prevSibling else {
          return
        }
        if !firstNodeParent.canInsertTextBefore() {
          _ = try firstNodeParent.insertBefore(nodeToInsert: prevSibling)
        } else {
          _ = try firstNode.insertBefore(nodeToInsert: prevSibling)
        }
      }
      guard let prevSibling else {
        return
      }
      _ = try prevSibling.select(anchorOffset: nil, focusOffset: nil)
      firstNode = prevSibling
      if text != "" {
        try insertText(text)
        return
      }
    } else if firstNode.isSegmented() && startOffset != firstNodeTextLength {
      let textNode = createTextNode(text: firstNode.getTextContent(includeInert: false, includeDirectionless: true))
      _ = try firstNode.replace(replaceWith: textNode)
      firstNode = textNode
    }

    if selectedNodesLength == 1 {
      if isTokenOrInert(firstNode) {
        try firstNode.remove()
        return
      }
      let firstNodeFormat = firstNode.getFormat()

      if startOffset == endOffset && firstNodeFormat != format {
        if firstNode.getTextPart().isEmpty {
          firstNode = try firstNode.setFormat(format: format)
        } else {
          var textNode = createTextNode(text: text)
          textNode = try textNode.setFormat(format: format)
          _ = try textNode.select(anchorOffset: nil, focusOffset: nil)
          if startOffset == 0 {
            _ = try firstNode.insertBefore(nodeToInsert: textNode)
          } else {
            let targetNodeArray = try firstNode.splitText(splitOffsets: [startOffset])
            guard let targetNode = targetNodeArray.first else {
              throw LexicalError.invariantViolation("insertText: splitText returned no node")
            }
            try targetNode.insertAfter(nodeToInsert: textNode)
          }
          return
        }
      }
      let delCount = endOffset - startOffset

      firstNode = try firstNode.spliceText(offset: startOffset, delCount: delCount, newText: text, moveSelection: true)

      if firstNode.getTextPart().isEmpty {
        try firstNode.remove()
      } else if firstNode.isComposing() && anchor.type == .text {
        anchor.offset -= text.lengthAsNSString()
      }
    } else {
      let lastIndex = selectedNodesLength - 1
      var lastNode = selectedNodes[lastIndex]
      var markedNodeKeysForKeep: Set<String> = Set(firstNode.getParentKeys())
      lastNode.getParentKeys().forEach({ markedNodeKeysForKeep.insert($0) })

      // First node is a TextNode, so we're getting a new "firstNode" from the start of selectedNodes
      let firstElement: ElementNode
      if let elementNode = selectedNodes[0] as? ElementNode {
        firstElement = elementNode
      } else {
        firstElement = try firstNode.getParentOrThrow()
      }

      let lastElement: ElementNode = try (lastNode as? ElementNode ?? lastNode.getParentOrThrow())

      // Handle mutations to the last node.
      if let lastIndex = lastNode.getIndexWithinParent() {
        if endPoint.type == .text && (endOffset != 0 || lastNode.getTextPart().isEmpty) ||
            (endPoint.type == .element && lastIndex < endOffset) {
          if let lastTextNode = lastNode as? TextNode,
             !isTokenOrInert(lastTextNode) && endOffset != lastTextNode.getTextContentSize() {
            if lastTextNode.isSegmented() {
              let textNode = createTextNode(text: lastNode.getTextContent())
              _ = try lastNode.replace(replaceWith: textNode)
              lastNode = textNode
            }
            lastNode = try lastTextNode.spliceText(offset: 0, delCount: endOffset, newText: "")
            markedNodeKeysForKeep.insert(lastNode.getKey())
          } else {
            if format != firstNode.format, let lastTextNode = lastNode as? TextNode {
              lastNode = try lastTextNode.spliceText(offset: 0, delCount: endOffset, newText: text, moveSelection: true)
              markedNodeKeysForKeep.insert(lastNode.getKey())
            } else {
              try lastNode.remove()
            }
          }
        } else {
          markedNodeKeysForKeep.insert(lastNode.getKey())
        }
      }

      // Either move the remaining nodes of the last parent to after
      // the first child, or remove them entirely. If the last parent
      // is the same as the first parent, this logic also works.
      let lastNodeChildren = lastElement.getChildren().reversed()
      let selectedNodesSet = Set(selectedNodes)
      let firstAndLastElementsAreEqual = firstElement === lastElement

      // If the last element is an "inline" element, don't move it's text nodes to the first node.
      // Instead, preserve the "inline" element's children and append to the first element.
      if !lastElement.canBeEmpty() {
        try firstElement.append([lastElement])
      } else {
        for node in lastNodeChildren {
          if node === firstNode {
            break
          }

          if node.isAttached() {
            if !selectedNodesSet.contains(node) || node === lastNode {
              if !firstAndLastElementsAreEqual {
                _ = try firstNode.insertAfter(nodeToInsert: node)
              }
            } else {
              try node.remove()
            }
          }
        }

        if !firstAndLastElementsAreEqual {
          // Check if we have already moved out all the nodes of the
          // last parent, and if so, traverse the parent tree and mark
          // them all as being able to deleted too.
          var parent: Node? = lastElement
          var lastRemovedParent: Node?

          while let unwrappedParent = parent as? ElementNode {
            let children = unwrappedParent.getChildren()
            let childrenLength = children.count

            if childrenLength == 0 || children[childrenLength - 1].isSameKey(lastRemovedParent) {
              markedNodeKeysForKeep.remove(unwrappedParent.getKey())
              lastRemovedParent = unwrappedParent
            }

            parent = unwrappedParent.getParent()
          }
        }
      }

      if firstNode.format == format {
        // Ensure we do splicing after moving of nodes, as splicing
        // can have side-effects (in the case of hashtags).
        if !isTokenOrInert(firstNode) {
          firstNode = try firstNode.spliceText(
            offset: startOffset,
            delCount: firstNodeTextLength - startOffset,
            newText: text,
            moveSelection: true
          )
          if firstNode.getTextPart().isEmpty {
            try firstNode.remove()
          } else if firstNode.isComposing() && anchor.type == .text {
            anchor.offset -= text.lengthAsNSString()
          }
        } else if startOffset == firstNodeTextLength {
          _ = try firstNode.select(anchorOffset: nil, focusOffset: nil)
        } else {
          try firstNode.remove()
        }
      }

      // Remove all selected nodes that haven't already been removed.
      for node in selectedNodes[1..<selectedNodes.count] {
        let key = node.getKey()

        if !markedNodeKeysForKeep.contains(key) &&
            (!isElementNode(node: node) || ((node as? ElementNode)?.canSelectionRemove() ?? false)) {
          try node.remove()
        }
      }
    }
  }

  @discardableResult
  public func insertNodes(nodes: [Node], selectStart: Bool = false) throws -> Bool {
    if !isCollapsed() {
      try removeText()
    }

    let anchor = anchor
    let anchorOffset = anchor.offset
    let anchorNode = try anchor.getNode()
    var target = anchorNode

    if anchor.type == .element {
      if let element = try anchor.getNode() as? ElementNode {
        if let placementNode = element.getChildAtIndex(index: anchorOffset - 1) {
          target = placementNode
        } else {
          target = element
        }
      }
    }

    var siblings: [Node] = []

    let nextSiblings = anchorNode.getNextSiblings()
    let topLevelElement = anchorNode.getTopLevelElementOrThrow()

    if let anchorNode = anchorNode as? TextNode {
      let textContent = anchorNode.getTextPart()

      if anchorOffset == 0 && textContent.lengthAsNSString() != 0 {
        if let prevSibling = anchorNode.getPreviousSibling() {
          target = prevSibling
        } else {
          target = try anchorNode.getParentOrThrow()
        }

        siblings.append(anchorNode)
      } else if anchorOffset == textContent.lengthAsNSString() {
        target = anchorNode
      } else if isTokenOrInert(anchorNode) {
        return false
      } else {
        let danglingTextNodes = try anchorNode.splitText(splitOffsets: [anchorOffset])
        if danglingTextNodes.count == 1 {
          target = danglingTextNodes[0]
        } else {
          target = danglingTextNodes[0]
          siblings.append(contentsOf: Array(danglingTextNodes.dropFirst()))
        }
      }
    }

    let startingNode = target

    siblings.append(contentsOf: nextSiblings)

    let firstNode = nodes.first
    var didReplaceOrMerge = false

    for node in nodes {
      if let node = node as? ElementNode {
        if node == firstNode {
          if let unwrappedTarget = target as? ElementNode,
             unwrappedTarget.isEmpty() &&
              unwrappedTarget.canReplaceWith(replacement: node) {
            try target.replace(replaceWith: node)
            target = node
            didReplaceOrMerge = true
            continue
          }

          let firstDescendant = node.getFirstDescendant()

          if isLeafNode(firstDescendant) {
            guard var element = try firstDescendant?.getParentOrThrow() else {
              throw LexicalError.internal("Could not get firstDescendant's parent")
            }

            while element.isInline() {
              element = try element.getParentOrThrow()
            }

            let children = element.getChildren()

            if let target = target as? ElementNode {
              for child in children {
                try target.append([child])
              }
            } else {
              for child in children.reversed() {
                try target.insertAfter(nodeToInsert: child)
              }

              target = try target.getParentOrThrow()
            }

            try element.remove()
            didReplaceOrMerge = true

            if element === node {
              continue
            }
          }
        }

        if isTextNode(target) {
          target = topLevelElement
        }
      } else if didReplaceOrMerge && !isDecoratorNode(node) && isRootNode(node: target.getParent()) {
        throw LexicalError.invariantViolation("insertNodes: cannot insert a non-element into a root node")
      }

      didReplaceOrMerge = false

      if let unwrappedTarget = target as? ElementNode {
        if let node = node as? DecoratorNode, node.isTopLevel() {
          target = try target.insertAfter(nodeToInsert: node)
        } else if !isElementNode(node: node) {
          if let firstChild = unwrappedTarget.getFirstChild() {
            try firstChild.insertBefore(nodeToInsert: node)
          } else {
            try unwrappedTarget.append([node])
          }

          target = node
        } else {
          if let elementNode = node as? ElementNode {
            if !elementNode.canBeEmpty() && elementNode.isEmpty() {
              continue
            }

            target = try target.insertAfter(nodeToInsert: node)
          }
        }
      } else if !isElementNode(node: node) ||
                  isDecoratorNode(node) && (node as? DecoratorNode)?.isTopLevel() == true {
        target = try target.insertAfter(nodeToInsert: node)
      } else {
        target = try node.getParentOrThrow() // Re-try again with the target being the parent
        continue
      }
    }

    if selectStart {
      if isTextNode(startingNode) {
        if let startingNode = startingNode as? ElementNode {
          try startingNode.select(anchorOffset: nil, focusOffset: nil)
        }
      } else {
        let prevSibling = target.getPreviousSibling()

        if isTextNode(prevSibling) {
          if let prevSibling = prevSibling as? ElementNode {
            try prevSibling.select(anchorOffset: nil, focusOffset: nil)
          }
        } else {
          let index = target.getIndexWithinParent()
          try target.getParentOrThrow().select(anchorOffset: index, focusOffset: index)
        }
      }
    }

    if let unwrappedTarget = target as? ElementNode {
      let lastChild = unwrappedTarget.getLastDescendant()

      if !selectStart {
        if lastChild == nil {
          try unwrappedTarget.select(anchorOffset: nil, focusOffset: nil)
        } else if let lastChild = lastChild as? TextNode {
          try lastChild.select(anchorOffset: nil, focusOffset: nil)
        } else {
          _ = try lastChild?.selectNext(anchorOffset: nil, focusOffset: nil)
        }
      }

      if siblings.count != 0 {
        for sibling in siblings.reversed() {
          let prevParent = try sibling.getParentOrThrow()

          if let unwrappedTarget = target as? ElementNode, !isElementNode(node: sibling) {
            try unwrappedTarget.append([sibling])
            target = sibling
          } else {
            if let elementSibling = sibling as? ElementNode, !elementSibling.canInsertAfter(node: target) {
              let prevParentClone = prevParent.clone()

              try prevParentClone.append([elementSibling])
              try target.insertAfter(nodeToInsert: prevParentClone)
            } else {
              try target.insertAfter(nodeToInsert: sibling)
            }
          }

          if prevParent.isEmpty() && !prevParent.canBeEmpty() {
            try prevParent.remove()
          }
        }
      }
    } else if !selectStart {
      if let target = target as? TextNode {
        try target.select(anchorOffset: nil, focusOffset: nil)
      } else {
        let element = try target.getParentOrThrow()
        if let index = target.getIndexWithinParent() {
          try element.select(anchorOffset: index + 1, focusOffset: index + 1)
        }
      }
    }
    return true
  }

  public func getPlaintext() throws -> String {
    // @alexmattice - replace this with a version driven off a depth first search
    guard let editor = getActiveEditor() else {
      throw LexicalError.invariantViolation("Requires editor")
    }

    let selection = editor.getNativeSelection()

    if let range = selection.range, let textStorage = editor.textStorage {
      return textStorage.attributedSubstring(from: range).string
    } else {
      return ""
    }
  }

  // MARK: - Internal

  internal func insertParagraph() throws {
    if !isCollapsed() {
      try removeText()
    }

    let anchorOffset = anchor.offset
    var currentElement: ElementNode
    var nodesToMove = [Node]()

    if anchor.type == .text {
      guard let anchorNode = try anchor.getNode() as? TextNode else { return }

      nodesToMove = anchorNode.getNextSiblings().reversed()
      currentElement = try anchorNode.getParentOrThrow()

      if anchorOffset == 0 {
        nodesToMove.append(anchorNode)
      } else if anchorOffset != anchorNode.getTextPartSize() {
        let splitNodes = try anchorNode.splitText(splitOffsets: [anchorOffset])
        if splitNodes.count >= 2 {
          nodesToMove.append(splitNodes[1])
        }
      }
    } else {
      guard let anchorNode = try anchor.getNode() as? ElementNode else { return }

      currentElement = anchorNode
      if let elementNode = currentElement as? RootNode {
        let paragraph = createParagraphNode()
        try paragraph.select(anchorOffset: nil, focusOffset: nil)

        if let child = elementNode.getChildAtIndex(index: anchorOffset) {
          try child.insertBefore(nodeToInsert: paragraph)
        } else {
          try elementNode.append([paragraph])
        }

        return
      }

      nodesToMove = currentElement.getChildren()
      nodesToMove.removeSubrange(0..<anchorOffset)
      nodesToMove.reverse()
    }

    let newElement = try currentElement.insertNewAfter(selection: self)
    if newElement == nil {
      // Handle as a line break insertion
      try insertLineBreak(selectStart: false)
    } else if let newElement = newElement as? ElementNode {
      // move the new element to be before the current element
      if anchorOffset == 0 && nodesToMove.count > 0 {
        try currentElement.insertBefore(nodeToInsert: newElement)
        return
      }

      var firstChild: Node?
      for nodeToMove in nodesToMove {
        if firstChild == nil {
          try newElement.append([nodeToMove])
        } else {
          _ = try firstChild?.insertBefore(nodeToInsert: nodeToMove)
        }

        firstChild = nodeToMove
      }

      if !newElement.canBeEmpty(), newElement.getChildrenSize() == 0 {
        try newElement.selectPrevious(anchorOffset: nil, focusOffset: nil)
        try newElement.remove()
      } else {
        let newSelection = try newElement.selectStart()
        anchor = newSelection.anchor
        focus = newSelection.focus
        dirty = newSelection.dirty
        format = newSelection.format
      }
    }
  }

  // Note that "line break" is different to "paragraph", and pressing return/enter does the latter.
  public func insertLineBreak(selectStart: Bool) throws {
    let lineBreakNode = createLineBreakNode()

    if anchor.type == .element {
      guard let element = try anchor.getNode() as? ElementNode else { return }
      if isRootNode(node: element) {
        try insertParagraph()
      }
    }
    if selectStart {
      _ = try insertNodes(nodes: [lineBreakNode], selectStart: true)
    } else {
      if try insertNodes(nodes: [lineBreakNode], selectStart: false) {
        _ = try lineBreakNode.selectNext(anchorOffset: 0, focusOffset: 0)
      }
    }
  }

  internal func deleteCharacter(isBackwards: Bool) throws {
    if isCollapsed() {
      let node = try anchor.getNode()

      if !isBackwards {
        var requiresCanExtractContents = false

        switch anchor.type {
        case .element:
          if let node = node as? ElementNode {
            requiresCanExtractContents = anchor.offset == node.getChildrenSize()
          }
        case .text:
          if let node = node as? TextNode {
            requiresCanExtractContents = anchor.offset == node.getTextContentSize()
          }
        case .range:
          throw LexicalError.invariantViolation("Need range selection")
        case .node:
          throw LexicalError.invariantViolation("Need node selection")
        case .grid:
          throw LexicalError.invariantViolation("Need grid selection")
        }

        if let nextSibling = try (node.getNextSibling() ?? (try node.getParentOrThrow()).getNextSibling()) as? ElementNode,
           requiresCanExtractContents && !nextSibling.canExtractContents() {
          return
        }
      }

      try modify(alter: .extend, isBackward: isBackwards, granularity: .character)

      if !isCollapsed() {
        let focusNode = focus.type == .text ? (try focus.getNode() as? TextNode) : nil
        let anchorNode = focus.type == .text ? (try anchor.getNode() as? TextNode) : nil

        if let node = focusNode, node.isSegmented() {
          let offset = focus.offset
          let textContentSize = node.getTextContentSize()

          if focusNode == anchorNode &&
              (isBackwards && offset != textContentSize) ||
              (!isBackwards && offset != 0) {
            try removeSegment(node: node, isBackward: isBackwards, offset: offset)
            return
          }
        } else if let node = anchorNode, node.isSegmented() {
          let offset = anchor.offset
          let textContentSize = node.getTextContentSize()

          if focusNode == anchorNode &&
              (isBackwards && offset != textContentSize) ||
              (!isBackwards && offset != 0) {
            try removeSegment(node: node, isBackward: isBackwards, offset: offset)
            return
          }
        }
        // @alexmattice - updateCaretSelectionForUnicodeCharacter(this, isBackward)
      } else if isBackwards && anchor.offset == 0 {
        // Special handling around rich text nodes
        let node = try anchor.type == .element ? anchor.getNode() : (try anchor.getNode().getParentOrThrow())

        if let elementNode = node as? ElementNode, (try elementNode.collapseAtStart(selection: self)) {
          return
        }
      }
    }

    try removeText()
    // NOTE: This is where the logic for dealing with hashtags resides in the web code.
    // updateCaretSelectionForAdjacentHashtags()
  }

  public func deleteWord(isBackwards: Bool) throws {
    if isCollapsed() {
      try modify(alter: .extend, isBackward: isBackwards, granularity: .word)
    }
    try removeText()
  }

  public func deleteLine(isBackwards: Bool) throws {
    if isCollapsed() {
      try modify(alter: .extend, isBackward: isBackwards, granularity: .line)
    }
    try removeText()
  }

  internal func removeText() throws {
    try insertText("")
  }

  internal func modify(alter: NativeSelectionModificationType, isBackward: Bool, granularity: UITextGranularity) throws {
    let collapse = alter == .move

    guard let editor = getActiveEditor() else {
      throw LexicalError.invariantViolation("Cannot be called outside update loop")
    }

    editor.moveNativeSelection(
      type: alter,
      direction: isBackward ? .backward : .forward,
      granularity: granularity)

    let nativeSelection = editor.getNativeSelection()

    try applyNativeSelection(nativeSelection)

    // Because a range works on start and end, we might need to flip
    // to match the range has specifically.

    if let range = nativeSelection.range {
      let anchorLocation = isBackward ? range.location + range.length : range.location
      let focusLocation = isBackward ? range.location : range.location + range.length

      if !collapse && anchorLocation != anchor.offset || focusLocation != focus.offset {
        swapPoints()
      }
    }
  }

  // This method is the equivalent of applyDOMRange()
  public func applyNativeSelection(_ nativeSelection: NativeSelection) throws {
    guard let range = nativeSelection.range else { return }
    try applySelectionRange(range, affinity: range.length == 0 ? .backward : nativeSelection.affinity)
  }

  internal func applySelectionRange(_ range: NSRange, affinity: UITextStorageDirection) throws {
    guard let editor = getActiveEditor() else {
      throw LexicalError.invariantViolation("Calling applyNativeSelection when no active editor")
    }

    let anchorOffset = affinity == .forward ? range.location : range.location + range.length
    let focusOffset = affinity == .forward ? range.location + range.length : range.location

    if let anchor = try pointAtStringLocation(anchorOffset, searchDirection: affinity, rangeCache: editor.rangeCache),
       let focus = try pointAtStringLocation(focusOffset, searchDirection: affinity, rangeCache: editor.rangeCache) {
      self.anchor = anchor
      self.focus = focus
    }
  }

  init?(nativeSelection: NativeSelection) {
    guard let range = nativeSelection.range, let editor = getActiveEditor(), !nativeSelection.selectionIsNodeOrObject else { return nil }
    let affinity = range.length == 0 ? .backward : nativeSelection.affinity

    let anchorOffset = affinity == .forward ? range.location : range.location + range.length
    let focusOffset = affinity == .forward ? range.location + range.length : range.location

    guard let anchor = try? pointAtStringLocation(anchorOffset, searchDirection: affinity, rangeCache: editor.rangeCache),
          let focus = try? pointAtStringLocation(focusOffset, searchDirection: affinity, rangeCache: editor.rangeCache) else {
      return nil
    }

    self.anchor = anchor
    self.focus = focus
    self.dirty = false
    self.format = TextFormat()
  }

  internal func formatText(formatType: TextFormatType) throws {
    if isCollapsed() {
      toggleFormat(type: formatType)
      return
    }

    let selectedNodes = try getNodes()
    guard var firstNode = selectedNodes.first, let lastNode = selectedNodes.last else { return }

    var firstNextFormat = TextFormat()
    for node in selectedNodes {
      if let node = node as? TextNode {
        firstNextFormat = node.getFormatFlags(type: formatType)
        break
      }
    }

    let isBefore = try anchor.isBefore(point: focus)
    var startOffset = isBefore ? anchor.offset : focus.offset
    var endOffset = isBefore ? focus.offset : anchor.offset

    // This is the case where the user only selected the very end of the
    // first node so we don't want to include it in the formatting change.
    if startOffset == firstNode.getTextPartSize() {
      if let nextSibling = firstNode.getNextSibling() as? TextNode {
        // we basically make the second node the firstNode, changing offsets accordingly
        anchor.offset = 0
        startOffset = 0
        firstNode = nextSibling
        firstNextFormat = nextSibling.getFormat()
      }
    }

    // This is the case where we only selected a single node
    if firstNode === lastNode {
      if let textNode = firstNode as? TextNode {
        if anchor.type == .element && focus.type == .element {
          try textNode.setFormat(format: firstNextFormat)
          let newSelection = try textNode.select(anchorOffset: startOffset, focusOffset: endOffset)
          updateSelection(
            anchor: newSelection.anchor,
            focus: newSelection.focus,
            format: firstNextFormat,
            isDirty: newSelection.dirty)
          return
        }

        startOffset = anchor.offset > focus.offset ? focus.offset : anchor.offset
        endOffset = anchor.offset > focus.offset ? anchor.offset : focus.offset

        // No actual text is selected, so do nothing.
        if startOffset == endOffset {
          return
        }

        // The entire node is selected, so just format it
        if startOffset == 0 && endOffset == textNode.getTextPartSize() {
          try textNode.setFormat(format: firstNextFormat)
          let newSelection = try textNode.select(anchorOffset: startOffset, focusOffset: endOffset)
          updateSelection(
            anchor: newSelection.anchor,
            focus: newSelection.focus,
            format: firstNextFormat,
            isDirty: newSelection.dirty)
        } else {
          // node is partially selected, so split it into two nodes and style the selected one.
          let splitNodes = try textNode.splitText(splitOffsets: [startOffset, endOffset])
          let replacement = startOffset == 0 ? splitNodes[0] : splitNodes[1]
          try replacement.setFormat(format: firstNextFormat)
          let newSelection = try replacement.select(anchorOffset: 0, focusOffset: endOffset - startOffset)
          updateSelection(
            anchor: newSelection.anchor,
            focus: newSelection.focus,
            format: firstNextFormat,
            isDirty: newSelection.dirty)
        }

        format = firstNextFormat
      }
    } else {
      // multiple nodes selected
      if var textNode = firstNode as? TextNode {
        if startOffset != 0 {
          // the entire first node isn't selected, so split it
          let splitNodes = try textNode.splitText(splitOffsets: [startOffset])
          if splitNodes.count >= 1 {
            textNode = splitNodes[1]
          }

          startOffset = 0
        }
        try textNode.setFormat(format: firstNextFormat)

        // update selection
        if isBefore {
          anchor.updatePoint(key: textNode.key, offset: startOffset, type: .text)
        } else {
          focus.updatePoint(key: textNode.key, offset: startOffset, type: .text)
        }

        format = firstNextFormat
      }

      var lastNextFormat = firstNextFormat

      if var textNode = lastNode as? TextNode {
        lastNextFormat = textNode.getFormatFlags(type: formatType, alignWithFormat: firstNextFormat)
        // if the offset is 0, it means no actual characters are selected,
        // so we skip formatting the last node altogether.
        if endOffset != 0 {
          // if the entire last node isn't selected, split it
          if endOffset != textNode.getTextPartSize() {
            let lastNodes = try textNode.splitText(splitOffsets: [endOffset])
            if lastNodes.count >= 1 {
              textNode = lastNodes[0]
            }
          }

          try textNode.setFormat(format: lastNextFormat)
          // update selection
          if isBefore {
            focus.updatePoint(key: textNode.key, offset: endOffset, type: .text)
          } else {
            anchor.updatePoint(key: textNode.key, offset: endOffset, type: .text)
          }
        }
      }

      // deal with all the nodes in between
      for index in 1..<selectedNodes.count - 1 {
        let selectedNode = selectedNodes[index]
        let selectedNodeKey = selectedNode.getKey()

        if let textNode = selectedNode as? TextNode,
           selectedNodeKey != firstNode.getKey(),
           selectedNodeKey != lastNode.getKey() {
          let selectedNextFormat = textNode.getFormatFlags(type: formatType, alignWithFormat: lastNextFormat)
          try textNode.setFormat(format: selectedNextFormat)
        }
      }
    }
  }

  internal func clearFormat() {
    format = TextFormat()
  }

  // MARK: - Private

  private func updateSelection(anchor: Point, focus: Point, format: TextFormat, isDirty: Bool) {
    self.anchor.updatePoint(key: anchor.key, offset: anchor.offset, type: anchor.type)
    self.focus.updatePoint(key: focus.key, offset: focus.offset, type: focus.type)
    self.format = format
    self.dirty = isDirty
  }

  private func toggleFormat(type: TextFormatType) {
    format = toggleTextFormatType(format: format, type: type, alignWithFormat: nil)
    dirty = true
  }

  private func swapPoints() {
    let anchorKey = anchor.key
    let anchorOffset = anchor.offset
    let anchorType = anchor.type

    anchor.key = focus.key
    anchor.offset = focus.offset
    anchor.type = focus.type

    focus.key = anchorKey
    focus.offset = anchorOffset
    focus.type = anchorType
  }

  public func insertRawText(_ text: String) throws {
    let parts = text.split(whereSeparator: \.isNewline)
    if parts.count == 1 {
      try insertText(text)
      return
    }

    var nodesToInsert: [Node] = []
    for (i, part) in parts.enumerated() {
      let textNode = TextNode(text: String(part))
      nodesToInsert.append(textNode)
      if i < parts.count - 1 {
        nodesToInsert.append(LineBreakNode())
      }
    }
    try insertNodes(nodes: nodesToInsert)
  }

  public func isSelection(_ selection: BaseSelection) -> Bool {
    guard let selection = selection as? RangeSelection else {
      return false
    }
    return anchor == selection.anchor && focus == selection.focus && format == selection.format
  }
}

extension RangeSelection: Equatable {
  public static func == (lhs: RangeSelection, rhs: RangeSelection) -> Bool {
    return lhs.anchor == rhs.anchor &&
      lhs.focus == rhs.focus &&
      lhs.format == rhs.format
  }
}

extension RangeSelection: CustomDebugStringConvertible {
  public var debugDescription: String {
    return "\tanchor { \(anchor)\n\tfocus { \(focus)"
  }
}


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

import Foundation
import UIKit

func createPoint(key: NodeKey, offset: Int, type: SelectionType) -> Point {
  Point(key: key, offset: offset, type: type)
}

func selectPointOnNode(point: Point, node: Node) {
  var updatedOffset = point.offset

  if let textNode = node as? TextNode {
    let textContentLength = textNode.getTextPart().lengthAsNSString()

    if point.offset > textContentLength {
      updatedOffset = textContentLength
    }
  }

  point.updatePoint(key: node.key, offset: updatedOffset, type: point.type)
}

/// Returns the current Lexical selection, generating it from the UITextView if necessary.
/// - Parameters:
///   - allowInvalidPositions: In most cases, it is desirable to regenerate the selection whenever
///   the current selection does not refer to valid positions in valid nodes. However, there are some
///   situations, such as if you're getting the selection when preparing to modify it to be valid using your
///   own validity rules, when you just want to fetch the current selection whatever it is.
public func getSelection(allowInvalidPositions: Bool = false) throws -> BaseSelection? {
  let editorState = getActiveEditorState()
  let selection = editorState?.selection

  if let selection {
    if allowInvalidPositions == true {
      return selection
    }
    if sanityCheckSelection(selection) {
      return selection
    }
    getActiveEditor()?.log(.other, .warning, "Selection failed sanity check")
  }

  if let editor = getActiveEditor() {
    do {
      let selection = try createSelection(editor: editor)
      editorState?.selection = selection
      return selection
    } catch {
      editor.log(.other, .warning, "Exception while creating range selection")
      return nil
    }
  }

  // Could not get active editor. This is unexpected, but we can't log since logging requires editor!
  throw LexicalError.invariantViolation("called getSelection() without an active editor")
}

private func sanityCheckSelection(_ selection: BaseSelection) -> Bool {
  guard let selection = selection as? RangeSelection else {
    // no sanity checking on other selection types yet
    return true
  }

  let anchor = selection.anchor
  let focus = selection.focus

  if sanityCheckPoint(anchor) && sanityCheckPoint(focus) {
    return true
  }

  return false
}

private func sanityCheckPoint(_ point: Point) -> Bool {
  guard let node = getNodeByKey(key: point.key) else {
    return false
  }
  if point.type == .text {
    guard let node = node as? TextNode else {
      return false
    }
    let text = node.getText_dangerousPropertyAccess()
    guard point.offset <= text.lengthAsNSString() else {
      return false
    }
    return true
  } else if point.type == .element {
    guard let node = node as? ElementNode else {
      return false
    }
    guard point.offset <= node.getChildrenSize() else {
      return false
    }
    return true
  }
  getActiveEditor()?.log(.other, .error, "Points that are not text or element have not yet been implemented")
  return false
}

func adjustPointOffsetForMergedSibling(
  point: Point,
  isBefore: Bool,
  key: NodeKey,
  target: TextNode,
  textLength: Int) {
  if point.type == .text {
    point.key = key
    if !isBefore {
      point.offset += textLength
    }
  } else if point.offset > target.getIndexWithinParent() ?? 0 {
    point.offset -= 1
  }
}

func moveSelectionPointToSibling(
  point: Point,
  node: Node,
  parent: ElementNode) {
  var siblingKey: NodeKey?
  var offset = 0
  var type: SelectionType?

  if let prevSibling = node.getPreviousSibling() {
    siblingKey = prevSibling.key
    if let prevTextNode = prevSibling as? TextNode {
      offset = prevTextNode.getTextContentSize()
      type = .text
    } else if let prevElementNode = prevSibling as? ElementNode {
      offset = prevElementNode.getChildrenSize()
      type = .element
    }
  } else {
    if let nextSibling = node.getNextSibling() {
      siblingKey = nextSibling.key
      if isTextNode(nextSibling) {
        type = .text
      } else if isElementNode(node: nextSibling) {
        type = .element
      }
    }
  }

  if let siblingKey, let type {
    point.updatePoint(key: siblingKey, offset: offset, type: type)
  } else {
    if let offset = node.getIndexWithinParent() {
      point.updatePoint(key: parent.key, offset: offset, type: .element)
    } else {
      point.updatePoint(key: parent.key, offset: parent.getChildrenSize(), type: .element)
    }
  }
}

func editorStateHasDirtySelection(pendingEditorState: EditorState, editor: Editor) -> Bool {
  let currentSelection = editor.getEditorState().selection
  let pendingSelection = pendingEditorState.selection

  // Check if we need to update because of changes in selection
  if let pendingSelection {
    if pendingSelection.dirty {
      return true
    }
    if let currentSelection, !pendingSelection.isSelection(currentSelection) {
      return true
    }
  } else if currentSelection != nil {
    return true
  }

  return false
}

func stringLocationForPoint(_ point: Point, editor: Editor) throws -> Int? {
  let rangeCache = editor.rangeCache

  guard let rangeCacheItem = rangeCache[point.key] else { return nil }

  switch point.type {
  case .text:
    return rangeCacheItem.location + rangeCacheItem.preambleLength + point.offset
  case .element:
    guard let node = getNodeByKey(key: point.key) as? ElementNode else { return nil }

    let childrenKeys = node.getChildrenKeys()
    if point.offset > childrenKeys.count {
      return nil
    }

    if point.offset == childrenKeys.count {
      return rangeCacheItem.location + rangeCacheItem.preambleLength + rangeCacheItem.childrenLength
    }

    guard let childRangeCacheItem = rangeCache[childrenKeys[point.offset]] else { return nil }

    return childRangeCacheItem.location
  case .range:
    throw LexicalError.invariantViolation("Need range selection")
  case .node:
    throw LexicalError.invariantViolation("Need node selection")
  case .grid:
    throw LexicalError.invariantViolation("Need grid selection")
  }
}

func createNativeSelection(from selection: RangeSelection, editor: Editor) throws -> NativeSelection {
  let isBefore = try selection.anchor.isBefore(point: selection.focus)
  var affinity: UITextStorageDirection = isBefore ? .forward : .backward

  if selection.anchor == selection.focus {
    affinity = .forward
  }

  guard let anchorLocation = try stringLocationForPoint(selection.anchor, editor: editor),
        let focusLocation = try stringLocationForPoint(selection.focus, editor: editor)
  else {
    return NativeSelection()
  }

  let location = isBefore ? anchorLocation : focusLocation

  return NativeSelection(
    range: NSRange(location: location, length: abs(anchorLocation - focusLocation)),
    affinity: affinity)
}

func createEmptyRangeSelection() -> RangeSelection {
  let anchor = Point(key: kRootNodeKey, offset: 0, type: .element)
  let focus = Point(key: kRootNodeKey, offset: 0, type: .element)

  return RangeSelection(anchor: anchor, focus: focus, format: TextFormat())
}

/// When we create a selection, we try to use the previous selection where possible, unless an actual user selection change has occurred.
/// When we do need to create a new selection, we validate we can have text nodes for both anchor and focus nodes.
/// If that holds true, we then return that selection as a mutable object that we use for the editor state for this update cycle.
/// If a selection gets changed, and requires a update to native iOS selection, it gets marked as "dirty".
/// If the selection changes, but matches with the existing native selection, then we only need to sync it.
/// Otherwise, we generally bail out of doing an update to selection during reconciliation unless there are dirty nodes that need reconciling.
func createSelection(editor: Editor) throws -> BaseSelection? {
  let currentEditorState = editor.getEditorState()
  let lastSelection = currentEditorState.selection

  guard let lastSelection else {
    let nativeSelection = editor.getNativeSelection()

    if nativeSelection.selectionIsNodeOrObject {
      // cannot derive selection out of the UI layer in this case!
      return nil
    }

    let range = nativeSelection.range ?? NSRange(location: 0, length: 0)

    if let anchor = try pointAtStringLocation(range.location, searchDirection: nativeSelection.affinity, rangeCache: editor.rangeCache),
       let focus = try pointAtStringLocation(range.location + range.length, searchDirection: nativeSelection.affinity, rangeCache: editor.rangeCache) {
      return RangeSelection(anchor: anchor, focus: focus, format: TextFormat())
    }

    return nil
  }

  // we have a last selection. Clone it!
  return lastSelection.clone()
}

/// This is used to make a selection when the existing selection is null or should be replaced,
/// i.e. forcing selection on the editor when it current exists outside the editor.
func makeRangeSelection(
  anchorKey: NodeKey,
  anchorOffset: Int,
  focusKey: NodeKey,
  focusOffset: Int,
  anchorType: SelectionType,
  focusType: SelectionType) throws -> RangeSelection {
  guard let editorState = getActiveEditorState() else {
    throw LexicalError.internal("Editor state is nil")
  }

  let selection = RangeSelection(
    anchor: Point(key: anchorKey, offset: anchorOffset, type: anchorType),
    focus: Point(key: focusKey, offset: focusOffset, type: focusType),
    format: TextFormat())

  selection.dirty = true
  editorState.selection = selection

  return selection
}

func updateElementSelectionOnCreateDeleteNode(selection: RangeSelection,
                                              parentNode: Node,
                                              nodeOffset: Int,
                                              times: Int = 1) throws {
  let anchor = selection.anchor
  let focus = selection.focus
  let anchorNode = try anchor.getNode()
  let focusNode = try focus.getNode()
  if parentNode != anchorNode && parentNode != focusNode {
    return
  }

  let parentKey = parentNode.getKey()
  // Single node. We shift selection but never redimension it
  if selection.isCollapsed() {
    let selectionOffset = anchor.offset
    if nodeOffset <= selectionOffset {
      let newSelectionOffset = max(0, selectionOffset + times)
      anchor.updatePoint(key: parentKey, offset: newSelectionOffset, type: .element)
      focus.updatePoint(key: parentKey, offset: newSelectionOffset, type: .element)
      // The new selection might point to text nodes, try to resolve them
      try updateSelectionResolveTextNodes(selection: selection)
    }

    return
  }
  // Multiple nodes selected. We shift or redimension selection
  let isBackward = try selection.isBackward()
  let firstPoint = isBackward ? focus : anchor
  let firstPointNode = try firstPoint.getNode()
  let lastPoint = isBackward ? anchor : focus
  let lastPointNode = try lastPoint.getNode()
  if parentNode == firstPointNode {
    let firstPointOffset = firstPoint.offset
    if nodeOffset <= firstPointOffset {
      firstPoint.updatePoint(key: parentKey, offset: max(0, firstPointOffset + times), type: .element)
    }
  }

  if parentNode == lastPointNode {
    let lastPointOffset = lastPoint.offset
    if nodeOffset <= lastPointOffset {
      lastPoint.updatePoint(key: parentKey, offset: max(0, lastPointOffset + times), type: .element)
    }
  }
  // The new selection might point to text nodes, try to resolve them
  try updateSelectionResolveTextNodes(selection: selection)
}

func updateSelectionResolveTextNodes(selection: RangeSelection) throws {
  let anchor = selection.anchor
  let anchorOffset = anchor.offset
  let focus = selection.focus
  let focusOffset = focus.offset
  let anchorNode = try anchor.getNode() as? ElementNode
  let focusNode = try focus.getNode() as? ElementNode

  if selection.isCollapsed() {
    if isElementNode(node: anchorNode) {
      return
    }

    guard let childSize = anchorNode?.getChildrenSize() else { return }
    let anchorOffsetAtEnd = anchorOffset >= childSize
    guard let child = anchorOffsetAtEnd
            ? anchorNode?.getChildAtIndex(index: childSize - 1)
            : anchorNode?.getChildAtIndex(index: anchorOffset) else {
      return
    }

    if isTextNode(child) {
      var newOffset = 0
      if anchorOffsetAtEnd {
        newOffset = child.getTextPartSize()
      }
      anchor.updatePoint(key: child.getKey(), offset: newOffset, type: .text)
      focus.updatePoint(key: child.getKey(), offset: newOffset, type: .text)
    }

    return
  }

  if isElementNode(node: anchorNode) {
    guard let childSize = anchorNode?.getChildrenSize() else { return }

    let anchorOffsetAtEnd = anchorOffset >= childSize

    guard let child = anchorOffsetAtEnd
            ? anchorNode?.getChildAtIndex(index: childSize - 1)
            : anchorNode?.getChildAtIndex(index: anchorOffset)
    else {
      return
    }

    if isTextNode(child) {
      var newOffset = 0
      if anchorOffsetAtEnd {
        newOffset = child.getTextPartSize()
      }

      anchor.updatePoint(key: child.getKey(), offset: newOffset, type: .text)
    }
  }

  if isElementNode(node: focusNode) {
    guard let childSize = focusNode?.getChildrenSize() else { return }

    let focusOffsetAtEnd = focusOffset >= childSize
    guard let child = focusOffsetAtEnd
            ? focusNode?.getChildAtIndex(index: childSize - 1)
            : focusNode?.getChildAtIndex(index: focusOffset) else {
      return
    }

    if isTextNode(child) {
      var newOffset = 0
      if focusOffsetAtEnd {
        newOffset = child.getTextPartSize()
      }

      focus.updatePoint(key: child.getKey(), offset: newOffset, type: .text)
    }
  }
}

func moveSelectionPointToEnd(point: Point, node: Node) {
  if let node = node as? ElementNode {
    let lastNode = node.getLastDescendant()

    if isElementNode(node: lastNode) || isTextNode(lastNode) {
      if let lastNode {
        selectPointOnNode(point: point, node: lastNode)
      }
    } else {
      selectPointOnNode(point: point, node: node)
    }
  } else if let node = node as? TextNode {
    selectPointOnNode(point: point, node: node)
  }
}

func transferStartingElementPointToTextPoint(start: Point, end: Point, format: TextFormat) throws {
  guard let element = try start.getNode() as? ElementNode else { return }

  var placementNode = element.getChildAtIndex(index: start.offset)
  let textNode = try createTextNode(text: nil).setFormat(format: format)
  var target: Node

  if isRootNode(node: element) {
    let newParagraphNode = ParagraphNode()
    try newParagraphNode.append([textNode])
    target = newParagraphNode
  } else {
    target = textNode
  }

  _ = try textNode.setFormat(format: format)

  if placementNode == nil {
    try element.append([target])
  } else {
    placementNode = try placementNode?.insertBefore(nodeToInsert: target)
  }

  if start == end {
    end.updatePoint(key: textNode.getKey(), offset: 0, type: .text)
  }

  start.updatePoint(key: textNode.getKey(), offset: 0, type: .text)
}

func removeSegment(node: TextNode, isBackward: Bool, offset: Int) throws {
  let textNode = node
  let textContent = textNode.getTextContent(includeInert: false, includeDirectionless: true)
  var split: [String] = textContent
    .split(separator: " ")
    .enumerated()
    .map { String($0 > 0 ? " \($1)" : $1) }
  let splitLength = split.count
  var segmentOffset = 0
  var restoreOffset: Int?

  for (index, segment) in split.enumerated() {
    let isLastSegment = index == splitLength - 1
    restoreOffset = segmentOffset
    segmentOffset += segment.lengthAsNSString()

    if (isBackward && segmentOffset == offset) || segmentOffset > offset || isLastSegment {
      split.remove(at: index)
      if isLastSegment {
        restoreOffset = nil
      }
      break
    }
  }

  let nextTextContent = split
    .joined(separator: "")
    .trimmingCharacters(in: .whitespaces)

  if nextTextContent == "" {
    try textNode.remove()
  } else {
    try textNode.setText(nextTextContent)
    try textNode.select(anchorOffset: restoreOffset, focusOffset: restoreOffset)
  }
}

public func updateCaretSelectionForUnicodeCharacter(
  selection: RangeSelection,
  isBackward: Bool
) throws {
  let anchor = selection.anchor
  let focus = selection.focus
  let anchorNode = try anchor.getNode()
  let focusNode = try focus.getNode()

  if anchorNode == focusNode && anchor.type == .text && focus.type == .text {
    let anchorOffset = anchor.offset
    let focusOffset = focus.offset
    let isBefore = anchorOffset < focusOffset
    let startOffset = isBefore ? anchorOffset : focusOffset
    let endOffset = isBefore ? focusOffset : anchorOffset
    let characterOffset = endOffset - 1

    if startOffset != characterOffset {
      let text = anchorNode.getTextContent()
      let start = text.index(text.startIndex, offsetBy: startOffset)
      let end = text.index(text.startIndex, offsetBy: endOffset)
      let range = start..<end
      let textSlice = String(text[range])

      if !doesContainGrapheme(textSlice) {
        if isBackward {
          focus.offset = characterOffset
        } else {
          anchor.offset = characterOffset
        }
      }
    }
  } else {
    // TODO: Handling of multibyte characters
  }
}

public func setBlocksType(
  selection: RangeSelection,
  createElement: () -> ElementNode
) {
  if selection.anchor.key == kRootNodeKey {
    let element = createElement()
    guard let root = getRoot() else { return }
    let firstChild = root.getFirstChild()

    if let firstChild {
      _ = try? firstChild.replace(replaceWith: element, includeChildren: true)
    } else {
      try? root.append([element])
    }

    return
  }

  guard var nodes = try? selection.getNodes() else { return }
  var maybeBlock = try? selection.anchor.getNode().getParentOrThrow()

  if let maybeBlock, !nodes.contains(maybeBlock) {
    nodes.append(maybeBlock)
  }

  if let unwrappedMaybeBlock = maybeBlock, unwrappedMaybeBlock.isInline() {
    maybeBlock = try? unwrappedMaybeBlock.getParentOrThrow()

    if let maybeBlock, !nodes.contains(maybeBlock) {
      nodes.append(maybeBlock)
    }
  }

  for node in nodes {
    if !isBlock(node) {
      continue
    }

    let targetElement = createElement()
    if let node = node as? ElementNode {
      _ = try? targetElement.setIndent(node.getIndent())
    }
    _ = try? node.replace(replaceWith: targetElement, includeChildren: true)
  }
}

private func isBlock(_ node: Node) -> Bool {
  guard let node = node as? ElementNode, !isRootNode(node: node) else {
    return false
  }

  let firstChild = node.getFirstChild()
  let isLeafElement =
    firstChild == nil || isTextNode(firstChild) || ((firstChild as? ElementNode)?.isInline() ?? false)

  return !node.isInline() && node.canBeEmpty() != false && isLeafElement
}