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

import Foundation
import UIKit

enum AttributeUtils {
  static func attributedStringByAddingStyles(
    _ attributedString: NSAttributedString,
    from node: Node,
    state: EditorState,
    theme: Theme
  ) -> NSAttributedString {

    let combinedAttributes = attributedStringStyles(from: node, state: state, theme: theme)
    let length = attributedString.length

    guard let mutableCopy = attributedString.mutableCopy() as? NSMutableAttributedString else {
      // should never happen
      return NSAttributedString()
    }

    let copiedAttributes: NSDictionary = NSDictionary(dictionary: combinedAttributes)
    guard let copiedAttributesSwiftDict: [NSAttributedString.Key: Any] = copiedAttributes as? [NSAttributedString.Key: Any] else {
      return NSAttributedString()
    }

    // update font and rest of the attributes
    mutableCopy.addAttributes(copiedAttributesSwiftDict, range: NSRange(location: 0, length: length))

    guard let copiedString: NSAttributedString = mutableCopy.copy() as? NSAttributedString else {
      return NSAttributedString()
    }
    return copiedString
  }

  internal static func attributedStringStyles(
    from node: Node,
    state: EditorState,
    theme: Theme
  ) -> [NSAttributedString.Key: Any] {
    let lexicalAttributes = getLexicalAttributes(from: node, state: state, theme: theme).reversed()

    // combine all dictionaries and update the font style
    // leaf node's attributes have a priority over element node's attributes
    // hence, they are applied last
    var combinedAttributes = lexicalAttributes.reduce([:]) { $0.merging($1) { $1 } }

    var font = combinedAttributes[.font] as? UIFont ?? LexicalConstants.defaultFont
    var fontDescriptor = font.fontDescriptor
    var symbolicTraits = fontDescriptor.symbolicTraits

    // update symbolicTraits
    if let bold = combinedAttributes[.bold] as? Bool {
      if bold {
        symbolicTraits = symbolicTraits.union([.traitBold])
      } else {
        symbolicTraits = symbolicTraits.remove(.traitBold) ?? symbolicTraits
      }
    }

    if let italic = combinedAttributes[.italic] as? Bool {
      if italic {
        symbolicTraits = symbolicTraits.union([.traitItalic])
      } else {
        symbolicTraits = symbolicTraits.remove(.traitItalic) ?? symbolicTraits
      }
    }

    // update font face, family and size
    if let family = combinedAttributes[.fontFamily] as? String {
      fontDescriptor = fontDescriptor.withFamily(family)
    }

    if let size = coerceCGFloat(combinedAttributes[.fontSize]) {
      fontDescriptor = fontDescriptor.addingAttributes([.size: size])
    }

    fontDescriptor = fontDescriptor.withSymbolicTraits(symbolicTraits) ?? fontDescriptor
    font = UIFont(descriptor: fontDescriptor, size: 0)

    combinedAttributes[.font] = font

    if let paragraphStyle = getParagraphStyle(attributes: combinedAttributes, indentSize: CGFloat(theme.indentSize)) {
      combinedAttributes[.paragraphStyle] = paragraphStyle
      combinedAttributes[.paragraphSpacingBefore_internal] = paragraphStyle.paragraphSpacingBefore
      combinedAttributes[.paragraphSpacing_internal] = paragraphStyle.paragraphSpacing
    }

    if combinedAttributes[.foregroundColor] == nil {
      combinedAttributes[.foregroundColor] = LexicalConstants.defaultColor
    }

    return combinedAttributes
  }

  static func getLexicalAttributes(
    from node: Node,
    state: EditorState,
    theme: Theme
  ) -> [[NSAttributedString.Key: Any]] {
    var node = node
    var attributes = [[NSAttributedString.Key: Any]]()
    attributes.append(node.getAttributedStringAttributes(theme: theme))
    if let elementNode = node as? ElementNode, elementNode.isInline() == false {
      attributes.append([.indent_internal: elementNode.getIndent()])
    }

    while let parent = node.parent, let parentNode = state.nodeMap[parent] {
      attributes.append(parentNode.getAttributedStringAttributes(theme: theme))
      if let elementNode = parentNode as? ElementNode, elementNode.isInline() == false {
        attributes.append([.indent_internal: elementNode.getIndent()])
      }
      node = parentNode
    }

    return attributes
  }

  private static func getParagraphStyle(attributes: [NSAttributedString.Key: Any], indentSize: CGFloat) -> NSParagraphStyle? {
    let paragraphStyle = NSMutableParagraphStyle()
    var styleFound = false

    var leftPadding: CGFloat = 0
    if let newPaddingHead = coerceCGFloat(attributes[.paddingHead]) {
      leftPadding += newPaddingHead
    }
    if let indent = attributes[.indent_internal] as? Int {
      leftPadding += CGFloat(indent) * indentSize
    }

    if leftPadding > 0 {
      paragraphStyle.firstLineHeadIndent = leftPadding
      paragraphStyle.headIndent = leftPadding
      styleFound = true
    }

    if let newPaddingTail = coerceCGFloat(attributes[.paddingTail]) {
      paragraphStyle.tailIndent = newPaddingTail
      styleFound = true
    }

    if let newLineHeight = coerceCGFloat(attributes[.lineHeight]) {
      paragraphStyle.minimumLineHeight = newLineHeight
      styleFound = true
    }

    if let newLineSpacing = coerceCGFloat(attributes[.lineSpacing]) {
      paragraphStyle.lineSpacing = newLineSpacing
      styleFound = true
    }

    if let paragraphSpacingBefore = coerceCGFloat(attributes[.paragraphSpacingBefore]) {
      paragraphStyle.paragraphSpacingBefore = paragraphSpacingBefore
      styleFound = true
    }

    return styleFound ? paragraphStyle : nil
  }

  private static func coerceCGFloat(_ object: Any?) -> CGFloat? {
    if let object = object as? Int {
      return CGFloat(object)
    }
    if let object = object as? Float {
      return CGFloat(object)
    }
    if let object = object as? CGFloat {
      return object
    }
    if let object = object as? Double {
      return CGFloat(object)
    }
    return nil
  }

  private static func extraLineFragmentIsPresent(_ textStorage: TextStorage) -> Bool {
    let textAsNSString: NSString = textStorage.string as NSString
    guard textAsNSString.length > 0 else { return true }

    guard let scalar = Unicode.Scalar(textAsNSString.character(at: textAsNSString.length - 1)) else { return false }
    if NSCharacterSet.newlines.contains(scalar) { return true }
    return false
  }

  private enum BlockParagraphLocation {
    case range(NSRange, NSRange) // non-enclosing range, enclosing range
    case extraLineFragment
  }

  internal static func applyBlockLevelAttributes(_ attributes: BlockLevelAttributes, cacheItem: RangeCacheItem, textStorage: TextStorage, nodeKey: NodeKey, lastDescendentAttributes: [NSAttributedString.Key: Any]) {

    // for more information about the extraLineFragment, see NSLayoutManager docs
    let extraLineFragmentIsPresent = extraLineFragmentIsPresent(textStorage)
    let startTouchesExtraLineFragment = (extraLineFragmentIsPresent && cacheItem.range.length == 0 && cacheItem.range.location == textStorage.length)
    let endTouchesExtraLineFragment = (extraLineFragmentIsPresent && (NSMaxRange(cacheItem.range) - cacheItem.postambleLength) == textStorage.length) // ignore postamble, since postamble terminates the block

    var extraLineFragmentAttributes = (extraLineFragmentIsPresent) ? lastDescendentAttributes : [:]

    var paragraphs: [BlockParagraphLocation] = []

    if startTouchesExtraLineFragment {
      paragraphs.append(.extraLineFragment)
    } else {
      // this time we _don't_ ignore postamble, since we want the applied styles to touch the newlines, and enumerateSubstrings handles trimming newlines anyway
      textStorage.mutableString.enumerateSubstrings(in: cacheItem.range, options: .byParagraphs) { _, substringRange, enclosingRange, _ in
        paragraphs.append(.range(substringRange, enclosingRange))
      }
      if endTouchesExtraLineFragment {
        paragraphs.append(.extraLineFragment)
      }
    }

    // first may be the same as last. That's OK!
    guard let first = paragraphs.first, let last = paragraphs.last else {
      return
    }

    var firstParaStyle: NSParagraphStyle
    let spacingBeforeInternal: CGFloat?
    switch first {
    case .extraLineFragment:
      firstParaStyle = extraLineFragmentAttributes[.paragraphStyle] as? NSParagraphStyle ?? NSParagraphStyle()
      spacingBeforeInternal = extraLineFragmentAttributes[.paragraphSpacingBefore_internal] as? CGFloat
    case .range(let nonEnclosing, _):
      firstParaStyle = textStorage.attribute(.paragraphStyle, at: nonEnclosing.location, effectiveRange: nil) as? NSParagraphStyle ?? NSParagraphStyle()
      spacingBeforeInternal = textStorage.attribute(.paragraphSpacingBefore_internal, at: nonEnclosing.location, effectiveRange: nil) as? CGFloat
    }

    guard let firstMutableParaStyle = firstParaStyle.mutableCopy() as? NSMutableParagraphStyle else {
      return
    }
    var spacingBefore = spacingBeforeInternal ?? firstMutableParaStyle.paragraphSpacingBefore
    spacingBefore += attributes.marginTop
    spacingBefore += attributes.paddingTop
    firstMutableParaStyle.paragraphSpacingBefore = spacingBefore

    switch first {
    case .extraLineFragment:
      extraLineFragmentAttributes[.paragraphStyle] = firstMutableParaStyle
    case .range(_, let enclosing):
      textStorage.addAttribute(.paragraphStyle, value: firstMutableParaStyle, range: enclosing)
    }

    // Now do the same for 'last'. This may involve reading back out the para style we just set for first, and modifying it further.

    var lastParaStyle: NSParagraphStyle
    let spacingInternal: CGFloat?

    switch last {
    case .extraLineFragment:
      lastParaStyle = extraLineFragmentAttributes[.paragraphStyle] as? NSParagraphStyle ?? NSParagraphStyle()
      spacingInternal = extraLineFragmentAttributes[.paragraphSpacing_internal] as? CGFloat
    case .range(let nonEnclosing, _):
      lastParaStyle = textStorage.attribute(.paragraphStyle, at: nonEnclosing.location, effectiveRange: nil) as? NSParagraphStyle ?? NSParagraphStyle()
      spacingInternal = textStorage.attribute(.paragraphSpacingBefore_internal, at: nonEnclosing.location, effectiveRange: nil) as? CGFloat
    }

    guard let lastMutableParaStyle = lastParaStyle.mutableCopy() as? NSMutableParagraphStyle else {
      return
    }
    var spacingAfter = spacingInternal ?? lastMutableParaStyle.paragraphSpacing
    spacingAfter += attributes.marginBottom
    spacingAfter += attributes.paddingBottom
    lastMutableParaStyle.paragraphSpacing = spacingAfter

    switch last {
    case .extraLineFragment:
      extraLineFragmentAttributes[.paragraphStyle] = lastMutableParaStyle
    case .range(_, let enclosing):
      textStorage.addAttribute(.paragraphStyle, value: lastMutableParaStyle, range: enclosing)
    }

    if extraLineFragmentIsPresent {
      textStorage.extraLineFragmentAttributes = extraLineFragmentAttributes
    } else {
      textStorage.extraLineFragmentAttributes = nil
    }

    // see comment on `appliedBlockLevelStyles_internal`. We're only doing this to provide data to our custom drawing routines.
    if !startTouchesExtraLineFragment, case .range(_, let enclosing) = first {
      textStorage.addAttribute(.appliedBlockLevelStyles_internal, value: attributes, range: enclosing)
    }
  }
}

extension NSAttributedString.Key {
  internal static let indent_internal: NSAttributedString.Key = .init(rawValue: "indent_internal")

  // These two properties are for Lexical to store the derived paragraph spacing. The reason for this is, when we calculate block
  // level styles, we need to read it back and adjust.
  internal static let paragraphSpacingBefore_internal: NSAttributedString.Key = .init(rawValue: "paragraphSpacingBefore_internal")
  internal static let paragraphSpacing_internal: NSAttributedString.Key = .init(rawValue: "paragraphSpacing_internal")

  // This attribute exists purely for storing the block level styles that have already been applied, so that the drawing code
  // within LayoutManager can read them back and pass them to any custom drawing functions. This attribute is not used to actually get
  // the spacing for margin/padding to be reserved, as they're already applied to the paragraph style!
  internal static let appliedBlockLevelStyles_internal: NSAttributedString.Key = .init(rawValue: "appliedBlockLevelStyles_internal")
}


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

internal func setPasteboard(selection: BaseSelection, pasteboard: UIPasteboard) throws {
  guard let editor = getActiveEditor() else {
    throw LexicalError.invariantViolation("Could not get editor")
  }
  let nodes = try generateArrayFromSelectedNodes(editor: editor, selection: selection).nodes
  let encodedData = try JSONEncoder().encode(nodes)
  guard let jsonString = String(data: encodedData, encoding: .utf8) else { return }

  let itemProvider = NSItemProvider()
  itemProvider.registerItem(forTypeIdentifier: LexicalConstants.pasteboardIdentifier) { completionHandler, expectedValueClass, options in
    let data = NSData(data: jsonString.data(using: .utf8) ?? Data())
    completionHandler?(data, nil)
  }

  pasteboard.items =
    [
      [(kUTTypeRTF as String): try getAttributedStringFromFrontend().data(
        from: NSRange(location: 0, length: getAttributedStringFromFrontend().length),
        documentAttributes: [.documentType: NSAttributedString.DocumentType.rtf])],
      [LexicalConstants.pasteboardIdentifier: encodedData]
    ]
}

internal func insertDataTransferForRichText(selection: RangeSelection, pasteboard: UIPasteboard) throws {
  let itemSet = pasteboard.itemSet(
    withPasteboardTypes: [
      (kUTTypeUTF8PlainText as String),
      (kUTTypeURL as String),
      LexicalConstants.pasteboardIdentifier
    ]
  )

  if let pasteboardData = pasteboard.data(
      forPasteboardType: LexicalConstants.pasteboardIdentifier,
      inItemSet: itemSet)?.last {
    let deserializedNodes = try JSONDecoder().decode(SerializedNodeArray.self, from: pasteboardData)

    guard let editor = getActiveEditor() else { return }

    _ = try insertGeneratedNodes(editor: editor, nodes: deserializedNodes.nodeArray, selection: selection)
    return
  }

  if let pasteboardRTFData = pasteboard.data(
      forPasteboardType: (kUTTypeRTF as String),
      inItemSet: itemSet)?.last {
    let attributedString = try NSAttributedString(
      data: pasteboardRTFData,
      options: [.documentType: NSAttributedString.DocumentType.rtf],
      documentAttributes: nil
    )

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

  if let pasteboardStringData = pasteboard.data(
      forPasteboardType: (kUTTypeUTF8PlainText as String),
      inItemSet: itemSet)?.last {
    try insertPlainText(selection: selection, text: String(decoding: pasteboardStringData, as: UTF8.self))
    return
  }

  if let url = pasteboard.urls?.first as? URL {
    let string = url.absoluteString
    try insertPlainText(selection: selection, text: string)
    return
  }
}

internal func insertPlainText(selection: RangeSelection, text: String) throws {
  var stringArray: [String] = []
  let range = text.startIndex..<text.endIndex
  text.enumerateSubstrings(in: range, options: .byParagraphs) { subString, _, _, _ in
    stringArray.append(subString ?? "")
  }

  if stringArray.count == 1 {
    try selection.insertText(text)
  } else {
    var nodes: [Node] = []
    var i = 0
    for part in stringArray {
      let textNode = createTextNode(text: String(part))
      if i != 0 {
        let paragraphNode = createParagraphNode()
        try paragraphNode.append([textNode])
        nodes.append(paragraphNode)
      } else {
        nodes.append(textNode)
      }
      i += 1
    }

    _ = try selection.insertNodes(nodes: nodes, selectStart: false)
  }
}

internal func insertRTF(selection: RangeSelection, attributedString: NSAttributedString) throws {
  let paragraphs = attributedString.splitByNewlines()

  var nodes: [Node] = []
  var i = 0

  for paragraph in paragraphs {
    var extractedAttributes = [(attributes: [NSAttributedString.Key: Any], range: NSRange)]()
    paragraph.enumerateAttributes(in: NSRange(location: 0, length: paragraph.length)) { (dict, range, stopEnumerating) in
      extractedAttributes.append((attributes: dict, range: range))
    }

    var nodeArray: [Node] = []
    for attribute in extractedAttributes {
      let text = paragraph.attributedSubstring(from: attribute.range).string
      let textNode = createTextNode(text: text)

      if (attribute.attributes.first(where: { $0.key == .font })?.value as? UIFont)?
          .fontDescriptor.symbolicTraits.contains(.traitBold) ?? false {
        textNode.format.bold = true
      }

      if (attribute.attributes.first(where: { $0.key == .font })?.value as? UIFont)?
          .fontDescriptor.symbolicTraits.contains(.traitItalic) ?? false {
        textNode.format.italic = true
      }

      if let underlineAttribute = attribute.attributes[.underlineStyle] {
        if underlineAttribute as? NSNumber != 0 {
          textNode.format.underline = true
        }
      }

      if let strikethroughAttribute = attribute.attributes[.strikethroughStyle] {
        if strikethroughAttribute as? NSNumber != 0 {
          textNode.format.strikethrough = true
        }
      }

      nodeArray.append(textNode)
    }

    if i != 0 {
      let paragraphNode = createParagraphNode()
      try paragraphNode.append(nodeArray)
      nodes.append(paragraphNode)
    } else {
      nodes.append(contentsOf: nodeArray)
    }
    i += 1
  }

  _ = try selection.insertNodes(nodes: nodes, selectStart: false)
}

public func insertGeneratedNodes(editor: Editor, nodes: [Node], selection: RangeSelection) throws {
  return try basicInsertStrategy(nodes: nodes, selection: selection)
}

func basicInsertStrategy(nodes: [Node], selection: RangeSelection) throws {
  var topLevelBlocks = [Node]()
  var currentBlock: ElementNode?
  for (index, _) in nodes.enumerated() {
    let node = nodes[index]
    if ((node as? ElementNode)?.isInline() ?? false) || isTextNode(node) || isLineBreakNode(node) {
      if let currentBlock {
        try currentBlock.append([node])
      } else {
        let paragraphNode = createParagraphNode()
        topLevelBlocks.append(paragraphNode)
        try paragraphNode.append([node])
        currentBlock = paragraphNode
      }
    } else {
      topLevelBlocks.append(node)
      currentBlock = nil
    }
  }

  _ = try selection.insertNodes(nodes: topLevelBlocks, selectStart: false)
}

func appendNodesToArray(
  editor: Editor,
  selection: BaseSelection?,
  currentNode: Node,
  targetArray: [Node] = []) throws -> (shouldInclude: Bool, outArray: [Node]) {
  var array = targetArray
  var shouldInclude = selection != nil ? try currentNode.isSelected() : true
  let shouldExclude = (currentNode as? ElementNode)?.excludeFromCopy() ?? false
  var clone = try cloneWithProperties(node: currentNode)
  (clone as? ElementNode)?.children = []

  if let textClone = clone as? TextNode {
    if let selection {
      clone = try sliceSelectedTextNodeContent(selection: selection, textNode: textClone)
    }
  }

  guard let key = try generateKey(node: clone) else {
    throw LexicalError.invariantViolation("Could not generate key")
  }
  clone.key = key
  editor.getEditorState().nodeMap[key] = clone

  let children = (currentNode as? ElementNode)?.getChildren() ?? []
  var cloneChildren: [Node] = []

  for childNode in children {
    let internalCloneChildren: [Node] = []
    let shouldIncludeChild = try appendNodesToArray(
      editor: editor,
      selection: selection,
      currentNode: childNode,
      targetArray: internalCloneChildren
    )

    if !shouldInclude && shouldIncludeChild.shouldInclude &&
        ((currentNode as? ElementNode)?.extractWithChild(child: childNode, selection: selection, destination: .clone) ?? false) {
      shouldInclude = true
    }

    cloneChildren.append(contentsOf: shouldIncludeChild.outArray)
  }

  for child in cloneChildren {
    (clone as? ElementNode)?.children.append(child.key)
  }

  if shouldInclude && !shouldExclude {
    array.append(clone)
  } else if let children = (clone as? ElementNode)?.children {
    for childKey in children {
      if let childNode = editor.getEditorState().nodeMap[childKey] {
        array.append(childNode)
      }
    }
  }

  return (shouldInclude, array)
}

public func generateArrayFromSelectedNodes(editor: Editor, selection: BaseSelection?) throws -> (
  namespace: String,
  nodes: [Node]) {
  var nodes: [Node] = []
  guard let root = getRoot() else {
    return ("", [])
  }
  for topLevelNode in root.getChildren() {
    var nodeArray: [Node] = []
    nodeArray = try appendNodesToArray(editor: editor, selection: selection, currentNode: topLevelNode, targetArray: nodeArray).outArray
    nodes.append(contentsOf: nodeArray)
  }
  return (
    namespace: "lexical",
    nodes
  )
}

// MARK: Extensions

extension NSAttributedString {
  public func splitByNewlines() -> [NSAttributedString] {
    var result = [NSAttributedString]()
    var rangeArray: [NSRange] = []

    (string as NSString).enumerateSubstrings(
      in: NSRange(location: 0, length: (string as NSString).length),
      options: .byParagraphs) { subString, subStringRange, enclosingRange, stop in
      rangeArray.append(subStringRange)
    }

    for range in rangeArray {
      let attributedString = attributedSubstring(from: range)
      result.append(attributedString)
    }
    return result
  }
}


/*
 * 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 extension CommandType {
  static let log = CommandType(rawValue: "log")
}

public enum LogFeature {
  case UITextView
  case NSTextStorage
  case node
  case editor
  case reconciler
  case TextView
  case other
}

public enum LogLevel: Int {
  case none = 0
  case error = 1
  case warning = 2
  case message = 3
  case verbose = 4
  case verboseIncludingUserContent = 5
}

public struct LogPayload {
  public let feature: LogFeature
  public let level: LogLevel
  public let string: String
  public let callingFunction: String
}

public extension Editor {
  func log(_ feature: LogFeature, _ level: LogLevel, _ string: String = "", _ callingFunction: String = #function) {
    let payload = LogPayload(feature: feature, level: level, string: string, callingFunction: callingFunction)
    self.dispatchCommand(type: .log, payload: payload)
  }
}


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

extension NSAttributedString.Key {
  public static let fontFamily: NSAttributedString.Key = .init(rawValue: "fontFamily")
  public static let fontSize: NSAttributedString.Key = .init(rawValue: "fontSize")
  public static let bold: NSAttributedString.Key = .init(rawValue: "bold")
  public static let italic: NSAttributedString.Key = .init(rawValue: "italic")
  // uppercase or lowercase
  public static let textTransform: NSAttributedString.Key = .init(rawValue: "textTransform")
  public static let paddingHead: NSAttributedString.Key = .init(rawValue: "paddingHead")
  public static let paddingTail: NSAttributedString.Key = .init(rawValue: "paddingTail")
  public static let lineHeight: NSAttributedString.Key = .init(rawValue: "lineHeight")
  public static let lineSpacing: NSAttributedString.Key = .init(rawValue: "lineSpacing")
  public static let paragraphSpacingBefore: NSAttributedString.Key = .init(rawValue: "paragraphSpacingBefore")
}


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

import Foundation

@objc public extension Editor {
  func registerCommandObjC(_ commandName: String, priority: CommandPriority, block: @escaping ((_ payload: Any?) -> Bool)) -> () -> Void {

    return self.registerCommand(type: CommandType(rawValue: commandName), listener: {payload in
      return block(payload)
    }, priority: priority)
  }

  func getTextContentObjC() -> String {
    var text: String = ""
    try? self.read {
      text = getRoot()?.getTextContent() ?? ""
    }
    return text
  }
}


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

// Leaving commented code for later when these properties/nodes are implemented
public func cloneWithProperties<T: Node>(node: T) throws -> Node {
  let latest = node.getLatest()
  let clone = latest.clone()
  clone.parent = latest.parent
  if let latestTextNode = latest as? TextNode,
     let latestCloneNode = clone as? TextNode {
    latestCloneNode.format = latestTextNode.format
    latestCloneNode.style = latestTextNode.style
    latestCloneNode.mode = latestTextNode.mode
    latestCloneNode.detail = latestTextNode.detail
    return latestCloneNode
  } else if let latestElementNode = latest as? ElementNode,
            let latestCloneNode = clone as? ElementNode {
    latestCloneNode.children = latestElementNode.children
    latestCloneNode.direction = latestElementNode.direction
    //    latestCloneNode.indent = latestElementNode.indent
    //    latestCloneNode.format = latestElementNode.format
    return latestCloneNode
    //  } else if ($isDecoratorNode(latest) && $isDecoratorNode(clone)) {
    //    clone.state = latest.state
  }

  return clone
}

func getIndexFromPossibleClone(
  node: Node,
  parent: ElementNode,
  nodeMap: [NodeKey: Node]
) -> Int? {
  let parentClone = nodeMap[parent.getKey()]
  if let parentElement = parentClone as? ElementNode {
    return parentElement.children.firstIndex(of: node.key)
  }
  return node.getIndexWithinParent()
}

// Only node that is excluded in JS currently is "LexicalOverflowNode"â€º
func getParentAvoidingExcludedElements(node: Node) -> ElementNode? {
  var parent = node.getParent()
  while let unwrappedParent = parent, unwrappedParent.excludeFromCopy() {
    parent = unwrappedParent.getParent()
  }

  return parent
}

func copyLeafNodeBranchToRoot(
  leaf: Node,
  startingOffset: Int,
  isLeftSide: Bool,
  range: [NodeKey],
  nodeMap: [NodeKey: Node]
) throws -> (range: [NodeKey], nodeMap: [NodeKey: Node]) {
  var mutableRange = range
  var mutableNodeMap = nodeMap

  var node: Node? = leaf
  var offset = startingOffset
  while let unwrappedNode = node {
    guard let parent = getParentAvoidingExcludedElements(node: unwrappedNode) else { break }

    if !((unwrappedNode as? ElementNode)?.excludeFromCopy() ?? false) || !isElementNode(node: unwrappedNode) {
      let key = unwrappedNode.getKey()
      var clone = mutableNodeMap[key]
      let needsClone = clone == nil
      if needsClone {
        clone = try cloneWithProperties(node: unwrappedNode)
        mutableNodeMap[key] = clone
      }
      if let textClone = clone as? TextNode, !textClone.isSegmented() && !textClone.isToken() {
        let textCloneText = textClone.getText_dangerousPropertyAccess() as NSString
        let length = textCloneText.length - (isLeftSide ? offset : 0) - (isLeftSide ? 0 : textCloneText.length - offset)

        let range = NSRange(location: isLeftSide ? offset : 0, length: length)
        let subString = textCloneText.substring(with: range)
        textClone.setText_dangerousPropertyAccess(String(subString))
        mutableNodeMap[key] = textClone
      } else if let elementClone = clone as? ElementNode {
        let start = isLeftSide ? offset : 0
        let end = isLeftSide ? elementClone.getChildrenSize() : offset + 1
        if elementClone.getChildrenSize() > 0 {
          elementClone.children = Array(elementClone.children[start..<end])
        }

        mutableNodeMap[key] = elementClone
      }
      if isRootNode(node: parent) {
        if needsClone {
          // We only want to collect a range of top level nodes.
          // So if the parent is the root, we know this is a top level.
          mutableRange.append(key)
        }
        break
      }
    }

    offset = getIndexFromPossibleClone(node: unwrappedNode, parent: parent, nodeMap: nodeMap) ?? -1
    node = parent
  }

  return (range: mutableRange, nodeMap: mutableNodeMap)
}

public func cloneContents(selection: RangeSelection) throws -> (
  nodeMap: [NodeKey: Node],
  range: [NodeKey]
) {
  let anchor = selection.anchor
  let focus = selection.focus
  let anchorOffset = anchor.getCharacterOffset()
  let focusOffset = focus.getCharacterOffset()
  let anchorNode = try anchor.getNode()
  let focusNode = try focus.getNode()
  let anchorNodeParent = try anchorNode.getParentOrThrow()

  // Handle a single text node extraction
  if let anchorTextNode = anchorNode as? TextNode,
     anchorNode.isSameKey(focusNode) &&
      (anchorNodeParent.canBeEmpty() || anchorNodeParent.getChildrenSize() > 1) {
    guard let clonedFirstNode = try cloneWithProperties(node: anchorTextNode) as? TextNode else {
      throw LexicalError.internal("Could not clone anchorNode as TextNode")
    }
    let isBefore = focusOffset > anchorOffset
    let textCloneText = clonedFirstNode.getText_dangerousPropertyAccess() as NSString
    let startOffset = isBefore ? anchorOffset : focusOffset
    let endOffset = isBefore ? focusOffset : anchorOffset
    let subString = textCloneText.substring(with: NSRange(location: startOffset, length: endOffset - startOffset))

    clonedFirstNode.setText_dangerousPropertyAccess(String(subString))

    let key = clonedFirstNode.getKey()
    return (nodeMap: [key: clonedFirstNode], range: [key])
  }
  var nodes = try selection.getNodes()
  if nodes.count == 0 {
    return (nodeMap: [:], range: [])
  }
  // Check if we can use the parent of the nodes, if the
  // parent can't be empty, then it's important that we
  // also copy that element node along with its children.
  var nodesLength = nodes.count
  let firstNode = nodes[0]
  if let firstNodeParent = firstNode.getParent(),
     (!firstNodeParent.canBeEmpty() || isRootNode(node: firstNodeParent)) {
    let parentChildren = firstNodeParent.children
    let parentChildrenLength = parentChildren.count
    if parentChildrenLength == nodesLength {
      var areTheSame = true
      for i in 0..<parentChildren.count {
        if parentChildren[i] != nodes[i].key {
          areTheSame = false
          break
        }
      }
      if areTheSame {
        nodesLength += 1
        nodes.append(firstNodeParent)
      }
    }
  }
  let lastNode = nodes[nodesLength - 1]
  let isBefore = try anchor.isBefore(point: focus)
  var nodeMap: [NodeKey: Node] = [:]
  var range: [NodeKey] = []

  // Do first node to root
  (range, nodeMap) = try copyLeafNodeBranchToRoot(
    leaf: firstNode,
    startingOffset: isBefore ? anchorOffset : focusOffset,
    isLeftSide: true,
    range: range,
    nodeMap: nodeMap
  )
  // Copy all nodes between
  for i in 0..<nodesLength - 1 {
    let node = nodes[i]
    let key = node.getKey()
    if !nodeMap.keys.contains(key) && (!((node as? ElementNode)?.excludeFromCopy() ?? false) || !isElementNode(node: node)) {
      let clone = try cloneWithProperties(node: node)
      if isRootNode(node: node.getParent()) {
        range.append(node.getKey())
      }
      nodeMap[key] = clone
    }
  }
  // Do last node to root
  (range, nodeMap) = try copyLeafNodeBranchToRoot(
    leaf: lastNode,
    startingOffset: isBefore ? focusOffset : anchorOffset,
    isLeftSide: false,
    range: range,
    nodeMap: nodeMap
  )

  var outputNodeArray: [(NodeKey, Node)] = nodeMap.map {
    ($0, $1)
  }

  outputNodeArray = outputNodeArray.sorted(by: { $0.0 < $1.0 })

  return (nodeMap, range)
}


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

/**
 Provides styling information for Lexical nodes.

 A Theme maps a ``NodeType`` to an array of NSAttributedString keys. It is up to the node class to decide what to do with this information. Most nodes will
 directly return the attributes in ``Node/getAttributedStringAttributes(theme:)``, passing them straight to TextKit, although this is not required.

 To set attributes on a theme, use the node type as if it were a property on the `Theme` class. For example:

 ```swift
 let theme: Theme()
 theme.paragraph = [
 .font: myFont,
 .lineHeight: 16.0,
 .foregroundColor: UIColor.black,
 ]
 ```

 Some nodes may require more than just one attributes dictionary. For example, a ``HeadingNode`` will want separate attributes for each possible heading size.
 To do this, use the ``Theme/setValue(_:forSubtype:value:)`` method. It is up to the node class to decide how to handle subtypes.

 ### Block level attributes

 There is some support for block level attributes, however this API could stand to be expanded in the future!

 For some context, TextKit 1 (which is what Lexical uses)
 lets attributes apply to a character range, or to a paragraph (by means of a subclass of `NSParagraphStyle`). A paragraph is defined
 by TextKit as a piece of text surrounded by line break characters.

 Consider, however, something like a code block. This is very likely to have multiple line breaks internally, but in order to style it, we might want to have
 a larger margin at the top of the code block and the bottom of the code block. Telling TextKit how to render this is difficult -- we have to set a paragraph style
 on the first paragraph of the code block with a bigger top margin, and a paragraph style on the last paragraph of the code block with a bigger bottom margin.

 Since Lexical wants to make a developer's life easier, we do this calculation internally. Our ``BlockLevelAttributes`` currently support margin and padding for top and
 bottom. This can be used in combination with Lexical's custom drawing support, to get the look you want. Apply block level attributes with ``Theme/setBlockLevelAttributes(_:value:)``.

 ## See Also

 - ``Node/getAttributedStringAttributes(theme:)``
 - ``Node/getBlockLevelAttributes(theme:)``
 */
@dynamicMemberLookup
@objc open class Theme: NSObject {
  public typealias AttributeDict = [NSAttributedString.Key: Any]
  private struct Key: Hashable, Equatable {
    internal init(_ nodeType: NodeType, _ subkey: String? = nil) {
      self.nodeType = nodeType
      self.subkey = subkey
    }

    let nodeType: NodeType
    let subkey: String?
  }

  /// The width in points of each indentation level.
  public var indentSize: Double = 40.0

  /// A set of attributes applied to a custom truncation indicator (a string that is displayed when the text overflows,
  /// such as an ellipsis or "See More" or similar).
  public var truncationIndicatorAttributes: AttributeDict = [:]

  private var attributes: [Key: AttributeDict] = [:]
  private var blockLevelAttributes: [NodeType: BlockLevelAttributes] = [:]

  public subscript(dynamicMember member: String) -> AttributeDict? {
    get {
      let nodeType: NodeType = NodeType(rawValue: member)
      return attributes[Key(nodeType)]
    }
    set {
      let nodeType: NodeType = NodeType(rawValue: member)
      attributes[Key(nodeType)] = newValue
    }
  }

  public func getValue(_ nodeType: NodeType, withSubtype subtype: String?) -> AttributeDict? {
    attributes[Key(nodeType, subtype)]
  }

  public func setValue(_ nodeType: NodeType, forSubtype subtype: String?, value: AttributeDict) {
    attributes[Key(nodeType, subtype)] = value
  }

  public func getBlockLevelAttributes(_ nodeType: NodeType) -> BlockLevelAttributes? {
    blockLevelAttributes[nodeType]
  }

  public func setBlockLevelAttributes(_ nodeType: NodeType, value: BlockLevelAttributes?) {
    blockLevelAttributes[nodeType] = value
  }
}


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

/// A Lexical Frontend is an object that contains the TextKit stack used by Lexical, along with handling
/// user interactions, incoming events, etc. The Frontend protocol provides a hard boundary for what are
/// the responsibilities of the Editor vs the Frontend.
///
/// For users of Lexical, it is expected that they will instantiate a Frontend, which will in turn set up
/// the TextKit stack and then instantiate an Editor. The Frontend should provide access to the editor for
/// users of Lexical.
///
/// In the future it will be possible to use Lexical without a Frontend, in Headless mode (allowing editing
/// an EditorState but providing no conversion to NSAttributedString).
internal protocol Frontend: AnyObject {
  var textStorage: TextStorage { get }
  var layoutManager: LayoutManager { get }
  var textContainerInsets: UIEdgeInsets { get }
  var editor: Editor { get }
  var nativeSelection: NativeSelection { get }
  var isFirstResponder: Bool { get }
  var viewForDecoratorSubviews: UIView? { get }
  var isEmpty: Bool { get }
  var isUpdatingNativeSelection: Bool { get set }
  var interceptNextSelectionChangeAndReplaceWithRange: NSRange? { get set }
  var textLayoutWidth: CGFloat { get }

  func moveNativeSelection(type: NativeSelectionModificationType, direction: UITextStorageDirection, granularity: UITextGranularity)
  func unmarkTextWithoutUpdate()
  func presentDeveloperFacingError(message: String)
  func updateNativeSelection(from selection: BaseSelection) throws
  func setMarkedTextFromReconciler(_ markedText: NSAttributedString, selectedRange: NSRange)
  func resetSelectedRange()
  func showPlaceholderText()
}


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

/// The LexicalViewDelegate allows customisation of certain things, most of which correspond to
/// UITextView delegate methods.
///
/// This protocol is somewhat a transitional thing. Eventually we would like to have every customisation point
/// for Lexical exposed through Lexical specific APIs, e.g. using Lexical commands and listeners as the customisation
/// mechanism.
public protocol LexicalViewDelegate: NSObjectProtocol {
  func textViewDidBeginEditing(textView: LexicalView)
  func textViewDidEndEditing(textView: LexicalView)
  func textViewShouldChangeText(_ textView: LexicalView, range: NSRange, replacementText text: String)
  func textView(_ textView: LexicalView, shouldInteractWith URL: URL, in selection: RangeSelection?, interaction: UITextItemInteraction) -> Bool
}

public extension LexicalViewDelegate {
  func textView(_ textView: LexicalView, shouldInteractWith URL: URL, in selection: RangeSelection?, interaction: UITextItemInteraction) -> Bool {
    return true
  }
}

public struct LexicalPlaceholderText {
  public var text: String
  public var font: UIFont
  public var color: UIColor

  public init(text: String, font: UIFont, color: UIColor) {
    self.text = text
    self.font = font
    self.color = color
  }
}

// MARK: -

/// A LexicalView is the view class that you interact with to use Lexical on iOS.
///
/// In order to avoid the possibility of accidentally using UITextView methods that Lexical does not expect, we've
/// encapsulated our UITextView subclass as a private property of LexicalView. The aim is to consider the UITextView
/// as below the abstraction level for developers using Lexical.
public class LexicalView: UIView, Frontend {
  var textLayoutWidth: CGFloat {
    return max(textView.bounds.width - textView.textContainerInset.left - textView.textContainerInset.right - 2 * textView.textContainer.lineFragmentPadding, 0)
  }

  let textView: TextView
  let responderForNodeSelection: ResponderForNodeSelection

  public init(editorConfig: EditorConfig, featureFlags: FeatureFlags, placeholderText: LexicalPlaceholderText? = nil) {
    self.textView = TextView(editorConfig: editorConfig, featureFlags: featureFlags)
    self.textView.showsVerticalScrollIndicator = false
    self.textView.clipsToBounds = true
    self.textView.accessibilityTraits = .staticText
    self.placeholderText = placeholderText

    guard let textStorage = textView.textStorage as? TextStorage else {
      fatalError()
    }
    self.responderForNodeSelection = ResponderForNodeSelection(editor: textView.editor, textStorage: textStorage)

    super.init(frame: .zero)

    self.textView.editor.frontend = self

    self.textView.lexicalDelegate = self
    if let placeholderText {
      self.textView.setPlaceholderText(placeholderText.text, textColor: placeholderText.color, font: placeholderText.font)
    }

    addSubview(self.textView)

    defaultViewMargins = textView.textContainerInset
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  // MARK: - Frontend protocol

  var textStorage: TextStorage {
    guard let textStorage = self.textView.textStorage as? TextStorage else {
      // this will never happen
      editor.log(.TextView, .error, "Text view had no text storage")
      fatalError()
    }
    return textStorage
  }

  var layoutManager: LayoutManager {
    guard let layoutManager = self.textView.layoutManager as? LayoutManager else {
      // this will never happen
      editor.log(.TextView, .error, "Text view had no layout manager")
      fatalError()
    }
    return layoutManager
  }

  var textContainerInsets: UIEdgeInsets {
    textView.textContainerInset
  }

  var nativeSelection: NativeSelection {
    if responderForNodeSelection.isFirstResponder {
      return NativeSelection(range: nil, opaqueRange: nil, affinity: .forward, markedRange: nil, markedOpaqueRange: nil, selectionIsNodeOrObject: true)
    }

    let selectionNSRange = textView.selectedRange
    let selectionOpaqueRange = textView.selectedTextRange
    let selectionAffinity = textView.selectionAffinity

    let markedOpaqueRange = textView.markedTextRange
    var markedNSRange: NSRange?
    if let markedOpaqueRange {
      let markedStart: Int = textView.offset(from: textView.beginningOfDocument, to: markedOpaqueRange.start)
      let markedEnd: Int = textView.offset(from: textView.beginningOfDocument, to: markedOpaqueRange.end)
      if markedStart != NSNotFound && markedEnd != NSNotFound {
        markedNSRange = NSRange(
          location: markedStart,
          length: markedEnd - markedStart)
      }
    }
    return NativeSelection(range: selectionNSRange, opaqueRange: selectionOpaqueRange, affinity: selectionAffinity, markedRange: markedNSRange, markedOpaqueRange: markedOpaqueRange, selectionIsNodeOrObject: false)
  }

  var isUpdatingNativeSelection: Bool {
    get {
      textView.isUpdatingNativeSelection
    }
    set {
      textView.isUpdatingNativeSelection = newValue
    }
  }

  var isEmpty: Bool {
    textView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
  }

  var viewForDecoratorSubviews: UIView? {
    textView
  }

  func moveNativeSelection(type: NativeSelectionModificationType, direction: UITextStorageDirection, granularity: UITextGranularity) {
    textView.isUpdatingNativeSelection = true

    let selection = nativeSelection

    guard let opaqueRange = selection.opaqueRange else {
      textView.isUpdatingNativeSelection = false
      return
    }

    var start = opaqueRange.start
    var end = opaqueRange.end

    switch direction {
    case .forward:
      end = textView.tokenizer.position(from: end, toBoundary: granularity, inDirection: UITextDirection(rawValue: UITextStorageDirection.forward.rawValue)) ?? end
      if type == .move {
        start = end
      }
    case .backward:
      start = textView.tokenizer.position(from: start, toBoundary: granularity, inDirection: UITextDirection(rawValue: UITextStorageDirection.backward.rawValue)) ?? start
      if type == .move {
        end = start
      }
    @unknown default:
      textView.isUpdatingNativeSelection = false
      return
    }

    let newTextRange = textView.textRange(from: start, to: end)
    textView.selectedTextRange = newTextRange
    textView.isUpdatingNativeSelection = false
  }

  func unmarkTextWithoutUpdate() {
    textView.unmarkTextWithoutUpdate()
  }

  func presentDeveloperFacingError(message: String) {
    textView.presentDeveloperFacingError(message: message)
  }

  func updateNativeSelection(from selection: BaseSelection) throws {
    guard let selection = selection as? RangeSelection else {
      // we don't have a range selection.
      responderForNodeSelection.becomeFirstResponder()
      return
    }
    try textView.updateNativeSelection(from: selection)
  }

  func setMarkedTextFromReconciler(_ markedText: NSAttributedString, selectedRange: NSRange) {
    textView.setMarkedTextFromReconciler(markedText, selectedRange: selectedRange)
  }

  func resetSelectedRange() {
    textView.resetSelectedRange()
  }

  var interceptNextSelectionChangeAndReplaceWithRange: NSRange? {
    get {
      textView.interceptNextSelectionChangeAndReplaceWithRange
    }
    set {
      textView.interceptNextSelectionChangeAndReplaceWithRange = newValue
    }
  }

  // MARK: - Other stuff

  public weak var delegate: LexicalViewDelegate?
  var defaultViewMargins: UIEdgeInsets = .zero

  /// Sets the accessibility identifier of the underlying text view
  public var textViewAccessibilityIdentifier: String? {
    get {
      textView.accessibilityIdentifier
    }
    set {
      textView.accessibilityIdentifier = newValue
    }
  }

  /// Sets the accessibility label of the underlying text view
  public var textViewAccessibilityLabel: String? {
    get {
      textView.accessibilityLabel
    }
    set {
      textView.accessibilityLabel = newValue
    }
  }

  /// Sets the background colour of the underlying text view
  public var textViewBackgroundColor: UIColor? {
    get {
      textView.backgroundColor
    }
    set {
      textView.backgroundColor = newValue
    }
  }

  /// Provides access to the ``EditorHistory`` instance for this LexicalView, for the purposes
  /// of applying commands to it (e.g. when setting up undo/redo listeners).
  ///
  /// This is an area that probably needs a refactor. Currently LexicalView provides a default ``EditorHistory``, but
  /// does not hook up the undo/redo/clear commands to it. This results in boilerplate code for any app wishing to
  /// provide undo/redo support. This could be improved!
  public lazy var editorHistory: EditorHistory = {
    EditorHistory(editor: self.textView.editor, externalHistoryState: createEmptyHistoryState())
  }()

  /// Configure the placeholder text shown by this Lexical view when there is no text.
  ///
  /// This needs a refactor. Currently the LexicalView supports setting the placeholder text as part of the initialiser, which
  /// works correctly. However setting the placeholder text later through this property will not properly proxy it through to the
  /// TextView. This is a bug and should be fixed.
  public var placeholderText: LexicalPlaceholderText?

  /// Returns the current selected text range according to the underlying UITextView.
  ///
  /// This needs a refactor. Probably this method should be deleted entirely (and the recommended approach for working with
  /// selections should be to go through the Lexical EditorState selection), unless a justification for its usefulness can be discovered.
  public var selectedTextRange: UITextRange? {
    get {
      textView.selectedTextRange
    }
  }

  /// Returns the attributed string fetched from the underlying text view's text storage.
  public var attributedText: NSAttributedString {
    get {
      textView.attributedText
    }
  }

  /// Returns the Lexical ``Editor`` owned by this LexicalView.
  ///
  /// This is the primary entry point for working with Lexical.
  public var editor: Editor {
    get {
      textView.editor
    }
  }

  /// A proxy for the underlying `UITextView`'s `isScrollEnabled` property.
  public var isScrollEnabled: Bool {
    get {
      textView.isScrollEnabled
    }

    set {
      textView.isScrollEnabled = newValue
    }
  }

  /// A shortcut for getting the selection position.
  ///
  /// This method should maybe not exist, we should consider this holistically when auditing our
  /// selection APIs. Currently it exists in order to support a feature in Work Chat, but if that's the only
  /// reason, we could move this method into Work Chat specific code.
  public lazy var cursorPosition: UInt = {
    guard let toCursorPosition = textView.selectedTextRange?.end else { return 0 }

    return UInt(textView.offset(from: textView.beginningOfDocument, to: toCursorPosition))
  }()

  /// Returns the marked text range from the underlying UITextView.
  ///
  /// Marked Text corresponds to Lexical's `composition`.
  public var markedTextRange: UITextRange? {
    get {
      textView.markedTextRange
    }
  }

  /// Returns the current text input mode from the underlying UITextView.
  ///
  /// This can be used to access the input language.
  public var textViewInputMode: UITextInputMode? {
    get {
      textView.textInputMode
    }
  }

  /// A convenience method for working out if there is any text in the text view.
  public var isTextViewEmpty: Bool {
    get {
      textView.text.lengthAsNSString() == 0
    }
  }

  /// A proxy for the underlying UITextView's `isFirstResponder` method
  public var textViewIsFirstResponder: Bool {
    get {
      textView.isFirstResponder
    }
  }

  /// A proxy for the underlying UITextView's CALayer's `borderWidth` property
  public var textViewBorderWidth: CGFloat {
    get {
      textView.layer.borderWidth
    }

    set {
      textView.layer.borderWidth = newValue
    }
  }

  /// A proxy for the underlying UITextView's CALayer's `borderColor` property.
  ///
  /// If refactoring, maybe we should make this API take a UIColor rather than a CGColor, to better
  /// match the rest of our UIKit-based APIs.
  public var textViewBorderColor: CGColor? {
    get {
      textView.layer.borderColor
    }
    set {
      textView.layer.borderColor = newValue
    }
  }

  // MARK: - Init

  override public func layoutSubviews() {
    super.layoutSubviews()

    textView.frame = CGRect(x: 0, y: 0, width: bounds.width, height: bounds.height)
  }

  /// Convenience method to clear editor and show placeholder text
  public func clearLexicalView() throws {
    try textView.defaultClearEditor()
    textView.showPlaceholderText()

    // these are necessary to reset the keyboard back to capital letters
    textView.inputDelegate?.selectionWillChange(textView)
    textView.inputDelegate?.selectionDidChange(textView)
  }

  public func textViewBecomeFirstResponder() {
    textView.becomeFirstResponder()
  }

  public func textViewResignFirstResponder() {
    textView.resignFirstResponder()
  }

  public func hideAccessoryInput(_ hidden: Bool) {
    textView.inputAccessoryView?.isHidden = hidden
  }

  // MARK: - TextView

  public func getTextViewSelectedRange() -> UITextRange? {
    textView.selectedTextRange
  }

  public func updateTextViewContentOffset() {
    let bottomOffset = CGPoint(
      x: 0,
      y: textView.contentSize.height - textView.bounds.size.height + textView.contentInset.bottom)

    textView.setContentOffset(bottomOffset, animated: false)
  }

  public func shouldInvalidateTextViewHeight(maxHeight: CGFloat) -> Bool {
    let textViewSize = textView.sizeThatFits(CGSize(width: textView.bounds.size.width, height: CGFloat.greatestFiniteMagnitude))
    let calculatedHeight = min(maxHeight, textViewSize.height)

    return calculatedHeight != textView.bounds.size.height
  }

  public func calculateTextViewHeight(for containerSize: CGSize, padding: UIEdgeInsets) -> CGFloat {
    let fullHeight = textView.sizeThatFits(
      CGSize(
        width: containerSize.width - padding.left - padding.right,
        height: containerSize.height - padding.top - padding.bottom)).height

    return fullHeight
  }

  public func showPlaceholderText() {
    textView.showPlaceholderText()
  }

  public func setTextContainerInset(_ margins: UIEdgeInsets) {
    textView.textContainerInset = margins
  }

  public func clearTextContainerInset() {
    textView.textContainerInset = defaultViewMargins
  }

  public func scrollSelectionToVisible() {
    textView.scrollRangeToVisible(textView.selectedRange)
  }

  // MARK: - Input Accessory View

  public func presentInputAccessoryView(view: UIView) {
    textView.inputAccessoryView = view
  }

  // MARK: - Paragraph Menu

  public func presentParagraphMenu(paragraphMenu: UIView) {
    UIView.performWithoutAnimation {
      textView.resignFirstResponder()
      textView.inputView = paragraphMenu
      textView.becomeFirstResponder()
    }
  }

  public func dismissParagraphMenu() {
    UIView.performWithoutAnimation {
      textView.resignFirstResponder()
      textView.inputView = nil
      textView.becomeFirstResponder()
    }
  }

  // MARK: - Autocomplete

  public func commitAutocompleteSuggestions() {
    textView.inputDelegate?.selectionWillChange(textView)
    textView.inputDelegate?.selectionDidChange(textView)
  }
}

// MARK: - LexicalTextViewDelegate

extension LexicalView: LexicalTextViewDelegate {
  func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
    var selection: RangeSelection?

    do {
      try editor.read {
        selection = try getSelection() as? RangeSelection ?? createEmptyRangeSelection()
        try selection?.applySelectionRange(characterRange, affinity: .forward)
      }
    } catch {
      print("Error received in LexicalView(shouldInteractWith): \(error.localizedDescription)")
    }

    return delegate?.textView(self, shouldInteractWith: URL, in: selection, interaction: interaction) ?? false
  }

  func textViewDidBeginEditing(textView: TextView) {
    delegate?.textViewDidBeginEditing(textView: self)
  }

  func textViewDidEndEditing(textView: TextView) {
    delegate?.textViewDidEndEditing(textView: self)
  }

  func textViewShouldChangeText(_ textView: UITextView, range: NSRange, replacementText text: String) {
    delegate?.textViewShouldChangeText(self, range: range, replacementText: text)
  }
}


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

class ResponderForNodeSelection: UIResponder, UIKeyInput {

  private weak var editor: Editor?
  private weak var textStorage: TextStorage?

  init(editor: Editor, textStorage: TextStorage) {
    self.editor = editor
    self.textStorage = textStorage
  }

  var hasText: Bool {
    true // if this class is being used, _something_ is selected from a Lexical point of view!
  }

  func insertText(_ text: String) {
    guard let editor, let textStorage else {
      return
    }
    textStorage.mode = TextStorageEditingMode.controllerMode
    editor.dispatchCommand(type: .insertText, payload: text)
    textStorage.mode = TextStorageEditingMode.none
  }

  func deleteBackward() {
    editor?.dispatchCommand(type: .deleteCharacter, payload: true)
  }
}


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

import UIKit

internal class LexicalReadOnlySizeCache {

  internal enum TruncationStringMode {
    case noTruncation
    case displayedInLastLine
    case displayedUnderLastLine
  }

  var requiredWidth: CGFloat = 0
  var requiredHeight: CGFloat? // nil if auto height
  var measuredTextKitHeight: CGFloat? // the height of rendered text. Will always be less than the targetHeight
  var customTruncationString: String? // set to nil to opt out of custom truncation
  var customTruncationAttributes: [NSAttributedString.Key: Any] = [:]
  var truncationStringMode: TruncationStringMode = .noTruncation // this is the computed truncation mode, not the desired mode
  var extraHeightForTruncationLine: CGFloat = 0 // iff truncationStringMode is displayedUnderLastLine, this is the height needed to add to the main height.
  var cachedTextContainerInsets: UIEdgeInsets = .zero
  var glyphRangeForLastLineFragmentBeforeTruncation: NSRange?
  var glyphRangeForLastLineFragmentAfterTruncation: NSRange?
  var characterRangeForLastLineFragmentBeforeTruncation: NSRange?
  var glyphIndexAtTruncationIndicatorCutPoint: Int? // assuming the truncation indicator is inline, this is where it would be cut.
  var textContainerDidShrinkLastLine: Bool?
  var sizeForTruncationString: CGSize?
  var originForTruncationStringInTextKitCoordinates: CGPoint? // need adding the insets left/top to get it in view coordinates
  var gapBeforeTruncationString: CGFloat = 6.0

  var completeHeightToRender: CGFloat {
    get {
      guard let measuredTextKitHeight else { return 0 }
      var height = measuredTextKitHeight

      // add insets if necessary
      height += cachedTextContainerInsets.top
      height += cachedTextContainerInsets.bottom

      if truncationStringMode == .displayedUnderLastLine {
        height += extraHeightForTruncationLine
      }

      return height
    }
  }

  var completeSizeToRender: CGSize {
    get {
      return CGSize(width: requiredWidth, height: completeHeightToRender)
    }
  }

  var customTruncationRect: CGRect? {
    get {
      guard let origin = originForTruncationStringInTextKitCoordinates,
            let size = sizeForTruncationString else { return nil }
      return CGRect(origin: origin, size: size)
    }
  }
}

@objc public class LexicalReadOnlyTextKitContext: NSObject, Frontend {
  @objc public let layoutManager: LayoutManager
  public let textStorage: TextStorage
  public let textContainer: TextContainer
  let layoutManagerDelegate: LayoutManagerDelegate
  @objc public let editor: Editor

  private var targetHeight: CGFloat? // null if fully auto-height
  internal var sizeCache: LexicalReadOnlySizeCache

  @objc public var truncationString: String?

  @objc weak var attachedView: LexicalReadOnlyView? {
    didSet {
      if oldValue == attachedView {
        return
      }
      if attachedView == nil {
        editor.frontendDidUnattachView()
      } else {
        editor.frontendDidAttachView()
      }
    }
  }
  @objc public init(editorConfig: EditorConfig, featureFlags: FeatureFlags) {
    layoutManager = LayoutManager()
    layoutManagerDelegate = LayoutManagerDelegate()
    layoutManager.delegate = layoutManagerDelegate
    textStorage = TextStorage()
    textStorage.addLayoutManager(layoutManager)
    textContainer = TextContainer()
    layoutManager.addTextContainer(textContainer)

    // TEMP
    textContainer.lineBreakMode = .byTruncatingTail

    sizeCache = LexicalReadOnlySizeCache()
    textContainer.readOnlySizeCache = sizeCache
    layoutManager.readOnlySizeCache = sizeCache

    editor = Editor(featureFlags: featureFlags, editorConfig: editorConfig)
    super.init()
    editor.frontend = self
    textStorage.editor = editor
  }

  internal func viewDidLayoutSubviews(viewBounds: CGRect) {
    setTextContainerSize(forWidth: viewBounds.width, maxHeight: self.targetHeight)
  }

  // MARK: - Size cache logistics

  private func createAndPropagateSizeCache() {
    sizeCache = LexicalReadOnlySizeCache()
    textContainer.readOnlySizeCache = sizeCache
    layoutManager.readOnlySizeCache = sizeCache
  }

  // MARK: - Size calculation

  let arbitrarilyLargeHeight: CGFloat = 100000

  @objc public func setTextContainerSizeWithUnlimitedHeight(forWidth width: CGFloat) {
    setTextContainerSize(forWidth: width, maxHeight: nil)
  }

  @objc public func setTextContainerSizeWithTruncation(forWidth width: CGFloat, maximumHeight maxHeight: CGFloat) {
    setTextContainerSize(forWidth: width, maxHeight: maxHeight)
  }

  private func setTextContainerSize(forWidth width: CGFloat, maxHeight: CGFloat?) {
    if sizeCache.requiredWidth == width && sizeCache.requiredHeight == maxHeight {
      return
    }

    createAndPropagateSizeCache()
    sizeCache.requiredWidth = width
    self.targetHeight = maxHeight
    sizeCache.requiredHeight = maxHeight
    sizeCache.customTruncationString = truncationString

    // 1. Set text container size
    let textContainerWidth = width - textContainerInsets.left - textContainerInsets.right
    sizeCache.cachedTextContainerInsets = textContainerInsets
    let textContainerHeight = maxHeight ?? arbitrarilyLargeHeight
    textContainer.size = CGSize(width: textContainerWidth, height: textContainerHeight)

    // we need a full re-lay-out here since size changed.
    layoutManager.invalidateLayout(forCharacterRange: NSRange(location: 0, length: textStorage.length), actualCharacterRange: nil)

    // 2. Get the last line fragment rect (pre truncation)
    let glyphRangeForContainer = layoutManager.glyphRange(for: textContainer)
    let lastGlyph = glyphRangeForContainer.upperBound - 1
    var effectiveGlyphRangeForLastLineFragmentPreTruncation = NSRange()
    let lastLineFragmentRect = layoutManager.lineFragmentRect(forGlyphAt: lastGlyph, effectiveRange: &effectiveGlyphRangeForLastLineFragmentPreTruncation)
    sizeCache.glyphRangeForLastLineFragmentBeforeTruncation = effectiveGlyphRangeForLastLineFragmentPreTruncation
    sizeCache.characterRangeForLastLineFragmentBeforeTruncation = layoutManager.characterRange(forGlyphRange: effectiveGlyphRangeForLastLineFragmentPreTruncation, actualGlyphRange: nil)

    // 3. Use the last line fragment rect to derive the used height. This will get replaced if we do truncation!
    sizeCache.measuredTextKitHeight = lastLineFragmentRect.maxY

    // 4. Is there truncation?
    let characterRangeForContainer = layoutManager.characterRange(forGlyphRange: glyphRangeForContainer, actualGlyphRange: nil)
    let isTruncating = self.truncationString != nil && characterRangeForContainer.upperBound < textStorage.string.lengthAsNSString()
    guard isTruncating, let truncationString else {
      return
    }

    // 5. If there's to be truncation, work out size of truncation string
    let truncationAttributes = editor.getTheme().truncationIndicatorAttributes
    let truncationAttributedString = NSAttributedString(string: truncationString, attributes: truncationAttributes)
    let truncationStringRect = truncationAttributedString.boundingRect(with: lastLineFragmentRect.size, options: .usesLineFragmentOrigin, context: nil)
    sizeCache.customTruncationAttributes = truncationAttributes
    sizeCache.sizeForTruncationString = truncationStringRect.size

    // 6. Now we've set the custom truncation string metrics on the size cache, if we re lay out, it should return different size.
    let characterRangeForLastLineFragmentPreTruncation = layoutManager.characterRange(forGlyphRange: effectiveGlyphRangeForLastLineFragmentPreTruncation, actualGlyphRange: nil)
    let truncationStringPlusGapLocation = CGRect(x: lastLineFragmentRect.width - truncationStringRect.width - sizeCache.gapBeforeTruncationString,
                                                 y: lastLineFragmentRect.minY,
                                                 width: truncationStringRect.width + sizeCache.gapBeforeTruncationString,
                                                 height: lastLineFragmentRect.height)
    let truncationCutPoint = layoutManager.glyphIndex(for: CGPoint(x: truncationStringPlusGapLocation.minX, y: lastLineFragmentRect.maxY - 1), in: textContainer, fractionOfDistanceThroughGlyph: nil)
    sizeCache.glyphIndexAtTruncationIndicatorCutPoint = truncationCutPoint

    layoutManager.invalidateLayout(forCharacterRange: characterRangeForLastLineFragmentPreTruncation, actualCharacterRange: nil)
    // at this point, consider the code flow as going to TextContainer.swift

    let newLastLineFragmentUsedRect = layoutManager.lineFragmentUsedRect(forGlyphAt: effectiveGlyphRangeForLastLineFragmentPreTruncation.lowerBound, effectiveRange: nil)

    // Replace the derived text kit height, in case something is different now!
    sizeCache.measuredTextKitHeight = newLastLineFragmentUsedRect.maxY

    // 7. now we detect truncation mode
    if let didShrink = sizeCache.textContainerDidShrinkLastLine, didShrink == true {
      // the line shrunk, so we must be truncating inline
      sizeCache.truncationStringMode = .displayedInLastLine
      sizeCache.originForTruncationStringInTextKitCoordinates = CGPoint(x: newLastLineFragmentUsedRect.maxX + sizeCache.gapBeforeTruncationString, y: newLastLineFragmentUsedRect.maxY - truncationStringRect.height)
    } else {
      // we know we're truncating due to step 4, but the last line seems not to have shrunk, so we must put See More on a new line.
      // To clarify, the shrinking happens in the logic inside TextContainer.swift
      sizeCache.truncationStringMode = .displayedUnderLastLine
      sizeCache.extraHeightForTruncationLine = truncationStringRect.height + sizeCache.gapBeforeTruncationString
      sizeCache.originForTruncationStringInTextKitCoordinates = CGPoint(x: 0, y: newLastLineFragmentUsedRect.maxY + sizeCache.gapBeforeTruncationString)
    }
  }

  @objc public func requiredSize() -> CGSize {
    return sizeCache.completeSizeToRender
  }

  var textLayoutWidth: CGFloat {
    get {
      return textContainer.size.width - 2 * textContainer.lineFragmentPadding
    }
  }

  @objc public var lineFragmentPadding: CGFloat {
    get {
      return self.textContainer.lineFragmentPadding
    }
    set {
      self.textContainer.lineFragmentPadding = newValue
    }
  }

  // MARK: - Frontend

  @objc public var textContainerInsets: UIEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)

  var nativeSelection: NativeSelection {
    NativeSelection()
  }

  var viewForDecoratorSubviews: UIView? {
    return self.attachedView
  }

  var isEmpty: Bool {
    return textStorage.length == 0
  }

  var isUpdatingNativeSelection: Bool = false

  var interceptNextSelectionChangeAndReplaceWithRange: NSRange?

  func moveNativeSelection(type: NativeSelectionModificationType, direction: UITextStorageDirection, granularity: UITextGranularity) {
    // no-op
  }

  func unmarkTextWithoutUpdate() {
    // no-op
  }

  func presentDeveloperFacingError(message: String) {
    // no-op
  }

  func updateNativeSelection(from selection: BaseSelection) throws {
    // no-op
  }

  func setMarkedTextFromReconciler(_ markedText: NSAttributedString, selectedRange: NSRange) {
    // no-op
  }

  func resetSelectedRange() {
    // no-op
  }

  func showPlaceholderText() {
    // no-op
  }

  var isFirstResponder: Bool {
    false
  }

  // MARK: - Drawing

  public func draw(inContext context: CGContext, point: CGPoint = .zero) {
    context.saveGState()
    UIGraphicsPushContext(context)

    let glyphRange = layoutManager.glyphRange(for: textContainer)
    let insetPoint = CGPoint(x: point.x + textContainerInsets.left, y: point.y + textContainerInsets.top)
    layoutManager.drawBackground(forGlyphRange: glyphRange, at: insetPoint)
    layoutManager.drawGlyphs(forGlyphRange: glyphRange, at: insetPoint)

    UIGraphicsPopContext()
    context.restoreGState()
  }
}


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

/// A Lexical frontend that is optimised for consumption. No scrolling, selection, or editing.
@objc public class LexicalReadOnlyView: UIView {

  // MARK: - Init

  override init(frame: CGRect) {
    super.init(frame: frame)

    let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
    self.addGestureRecognizer(tapGestureRecognizer)
    self.clipsToBounds = false
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  // MARK: -

  @objc public var textKitContext: LexicalReadOnlyTextKitContext? {
    willSet {
      if let oldContext = textKitContext, newValue !== textKitContext {
        oldContext.attachedView = nil
      }
    }
    didSet {
      textKitContext?.attachedView = self
      setNeedsDisplay()
    }
  }

  override public func layoutSubviews() {
    self.backgroundColor = .clear
    super.layoutSubviews()
    guard let textKitContext else {
      return
    }
    textKitContext.viewDidLayoutSubviews(viewBounds: self.bounds)
  }

  override public func draw(_ rect: CGRect) {
    if let textKitContext,
       let graphicsContext = UIGraphicsGetCurrentContext() {
      textKitContext.draw(inContext: graphicsContext)
    }
  }

  @objc func handleTap(gestureRecognizer: UITapGestureRecognizer) {
    guard let textKitContext else { return }
    let pointInView = gestureRecognizer.location(in: self)
    let pointInTextContainer = CGPoint(x: pointInView.x - textKitContext.textContainerInsets.left,
                                       y: pointInView.y - textKitContext.textContainerInsets.top)

    if let truncationRect = textKitContext.sizeCache.customTruncationRect {
      if truncationRect.contains(pointInTextContainer) {
        textKitContext.editor.dispatchCommand(type: .truncationIndicatorTapped, payload: nil)
        return
      }
    }

    let indexOfCharacter = textKitContext.layoutManager.characterIndex(for: pointInTextContainer,
                                                                       in: textKitContext.textContainer,
                                                                       fractionOfDistanceBetweenInsertionPoints: nil)
    let attributes = textKitContext.textStorage.attributes(at: indexOfCharacter, effectiveRange: nil)

    if let link = attributes[.link] {
      if let link = link as? URL {
        textKitContext.editor.dispatchCommand(type: .linkTapped, payload: link)
        return
      } else if let link = link as? String {
        if let linkURL = URL(string: link) {
          textKitContext.editor.dispatchCommand(type: .linkTapped, payload: linkURL)
          return
        }
      }
    }

    textKitContext.editor.dispatchCommand(type: .readOnlyViewTapped, payload: nil)
  }
}


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

import Foundation

@objc public protocol Plugin {
  func setUp(editor: Editor)
  func tearDown()
}

extension Plugin {
  public static func installedInstance() throws -> Self? {
    guard let editor = getActiveEditor() else {
      throw LexicalError.invariantViolation("Expected editor")
    }
    for plugin in editor.plugins {
      if let plugin = plugin as? Self {
        return plugin
      }
    }
    return nil
  }
}


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

import UIKit

public class LayoutManager: NSLayoutManager {
  internal weak var editor: Editor? {
    get {
      if let textStorage = textStorage as? TextStorage {
        return textStorage.editor
      }
      return nil
    }
  }

  internal var readOnlySizeCache: LexicalReadOnlySizeCache? // set to nil if not operating in read only mode

  private var customDrawingBackground: [NSAttributedString.Key: Editor.CustomDrawingHandlerInfo] {
    get {
      return editor?.customDrawingBackground ?? [:]
    }
  }
  private var customDrawingText: [NSAttributedString.Key: Editor.CustomDrawingHandlerInfo] {
    get {
      return editor?.customDrawingText ?? [:]
    }
  }

  override public func setExtraLineFragmentRect(
    _ fragmentRect: CGRect,
    usedRect: CGRect,
    textContainer container: NSTextContainer
  ) {
    // TODO: modify this based on our own metrics if needed -- @amyworrall
    super.setExtraLineFragmentRect(fragmentRect, usedRect: usedRect, textContainer: container)
  }

  override public func drawBackground(forGlyphRange drawingGlyphRange: NSRange, at origin: CGPoint) {
    super.drawBackground(forGlyphRange: drawingGlyphRange, at: origin)
    draw(forGlyphRange: drawingGlyphRange, at: origin, handlers: customDrawingBackground)
  }

  override public func drawGlyphs(forGlyphRange drawingGlyphRange: NSRange, at origin: CGPoint) {
    super.drawGlyphs(forGlyphRange: drawingGlyphRange, at: origin)
    draw(forGlyphRange: drawingGlyphRange, at: origin, handlers: customDrawingText)
    drawCustomTruncationIfNeeded(forGlyphRange: drawingGlyphRange, at: origin)
    positionAllDecorators()
  }

  private func drawCustomTruncationIfNeeded(forGlyphRange drawingGlyphRange: NSRange, at origin: CGPoint) {
    guard let readOnlySizeCache,
          let customTruncationRect = readOnlySizeCache.customTruncationRect,
          let string = readOnlySizeCache.customTruncationString else { return }

    let modifiedDrawingRect = customTruncationRect.offsetBy(dx: origin.x, dy: origin.y)

    let attributes = readOnlySizeCache.customTruncationAttributes
    let attributedString = NSAttributedString(string: string, attributes: attributes)
    attributedString.draw(in: modifiedDrawingRect)
  }

  private func draw(forGlyphRange drawingGlyphRange: NSRange, at origin: CGPoint, handlers: [NSAttributedString.Key: Editor.CustomDrawingHandlerInfo]) {
    let characterRange = characterRange(forGlyphRange: drawingGlyphRange, actualGlyphRange: nil)
    guard let textStorage = textStorage as? TextStorage else {
      return
    }

    handlers.forEach { attribute, value in
      let handler = value.customDrawingHandler
      let granularity = value.granularity

      textStorage.enumerateAttribute(attribute, in: characterRange) { value, attributeRunRange, _ in
        guard let value else {
          // we only trigger when there is a non-nil value
          return
        }
        let glyphRangeForAttributeRun = glyphRange(forCharacterRange: attributeRunRange, actualCharacterRange: nil)
        ensureLayout(forGlyphRange: glyphRangeForAttributeRun)

        switch granularity {
        case .characterRuns:
          enumerateLineFragments(forGlyphRange: glyphRangeForAttributeRun) { rect, usedRect, textContainer, glyphRangeForGlyphRun, _ in
            let intersectionRange = NSIntersectionRange(glyphRangeForAttributeRun, glyphRangeForGlyphRun)
            let charRangeToDraw = self.characterRange(forGlyphRange: intersectionRange, actualGlyphRange: nil)
            let glyphBoundingRect = self.boundingRect(forGlyphRange: intersectionRange, in: textContainer)
            handler(attribute, value, self, attributeRunRange, charRangeToDraw, intersectionRange, glyphBoundingRect.offsetBy(dx: origin.x, dy: origin.y), rect.offsetBy(dx: origin.x, dy: origin.y))
          }
        case .singleParagraph:
          let paraGroupRange = textStorage.mutableString.paragraphRange(for: attributeRunRange)
          (textStorage.string as NSString).enumerateSubstrings(in: paraGroupRange, options: .byParagraphs) { substring, substringRange, enclosingRange, _ in
            guard substringRange.length >= 1 else { return }
            let glyphRangeForParagraph = self.glyphRange(forCharacterRange: substringRange, actualCharacterRange: nil)
            let firstCharLineFragment = self.lineFragmentRect(forGlyphAt: glyphRangeForParagraph.location, effectiveRange: nil)
            let lastCharLineFragment = self.lineFragmentRect(forGlyphAt: glyphRangeForParagraph.upperBound - 1, effectiveRange: nil)
            let containingRect = firstCharLineFragment.union(lastCharLineFragment)
            handler(attribute, value, self, attributeRunRange, substringRange, glyphRangeForParagraph, containingRect.offsetBy(dx: origin.x, dy: origin.y), firstCharLineFragment.offsetBy(dx: origin.x, dy: origin.y))
          }
        case .contiguousParagraphs:
          let paraGroupRange = textStorage.mutableString.paragraphRange(for: attributeRunRange)
          guard paraGroupRange.length >= 1 else { return }
          let glyphRangeForParagraphs = self.glyphRange(forCharacterRange: paraGroupRange, actualCharacterRange: nil)
          let firstCharLineFragment = self.lineFragmentRect(forGlyphAt: glyphRangeForParagraphs.location, effectiveRange: nil)

          let lastCharLineFragment = (paraGroupRange.upperBound == textStorage.length && self.extraLineFragmentRect.height > 0)
            ? self.extraLineFragmentRect
            : self.lineFragmentRect(forGlyphAt: glyphRangeForParagraphs.upperBound - 1, effectiveRange: nil)

          var containingRect = firstCharLineFragment.union(lastCharLineFragment).offsetBy(dx: origin.x, dy: origin.y)

          // If there are block styles, subtract the margin here. TODO: support nested or overlapping block element styles
          if let blockStyle = textStorage.attribute(.appliedBlockLevelStyles_internal, at: paraGroupRange.location, effectiveRange: nil) as? BlockLevelAttributes {
            // first check to see if we should apply top margin.
            // Logic is, the margin size is taken into account in the fragment rect height by means of paragraphSpacingBefore... however, TextKit tries to be clever and omits that spacing
            // if it's the first paragraph.
            if paraGroupRange.location > 0 {
              containingRect.origin.y += blockStyle.marginTop
              containingRect.size.height -= blockStyle.marginTop
            }
            // next check for bottom margin
            if paraGroupRange.location + paraGroupRange.length < textStorage.string.lengthAsNSString() {
              containingRect.size.height -= blockStyle.marginBottom
            }
          }

          handler(attribute, value, self, attributeRunRange, paraGroupRange, glyphRangeForParagraphs, containingRect, firstCharLineFragment.offsetBy(dx: origin.x, dy: origin.y))
        }
      }
    }
  }

  private func positionAllDecorators() {
    guard let textStorage = textStorage as? TextStorage else { return }
    for (key, location) in textStorage.decoratorPositionCache {
      positionDecorator(forKey: key, characterIndex: location)
    }
  }

  private func positionDecorator(forKey key: NodeKey, characterIndex: TextStorage.CharacterLocation) {
    guard let textContainer = textContainers.first, let textStorage else {
      editor?.log(.TextView, .warning, "called with no container or storage")
      return
    }

    let glyphIndex = glyphIndexForCharacter(at: characterIndex)
    let glyphIsInTextContainer = NSLocationInRange(glyphIndex, glyphRange(for: textContainer))

    var glyphBoundingRect: CGRect = .zero
    let shouldHideView: Bool = !glyphIsInTextContainer

    if glyphIsInTextContainer {
      glyphBoundingRect = boundingRect(forGlyphRange: NSRange(location: glyphIndex, length: 1), in: textContainer)
    }

    var attribute: TextAttachment?

    if NSLocationInRange(characterIndex, NSRange(location: 0, length: textStorage.length)) {
      attribute = textStorage.attribute(.attachment, at: characterIndex, effectiveRange: nil) as? TextAttachment
    }

    guard let attr = attribute, let key = attr.key, let editor = attr.editor else {
      editor?.log(.TextView, .warning, "called with no attachment")
      return
    }

    let textContainerInset = self.editor?.frontend?.textContainerInsets ?? UIEdgeInsets.zero

    try? editor.read {
      guard let decoratorView = decoratorView(forKey: key, createIfNecessary: !shouldHideView) else {
        editor.log(.TextView, .warning, "create decorator view failed")
        return
      }

      if shouldHideView {
        decoratorView.isHidden = true
        return
      }

      // we have a valid location, make sure view is not hidden
      decoratorView.isHidden = false

      var decoratorOrigin = glyphBoundingRect.offsetBy(dx: textContainerInset.left, dy: textContainerInset.top).origin // top left

      decoratorOrigin.y += (glyphBoundingRect.height - attr.bounds.height) // bottom left now!

      decoratorView.frame = CGRect(origin: decoratorOrigin, size: attr.bounds.size)
    }
  }

  @available(iOS 13.0, *)
  override public func showCGGlyphs(_ glyphs: UnsafePointer<CGGlyph>, positions: UnsafePointer<CGPoint>, count glyphCount: Int, font: UIFont, textMatrix: CGAffineTransform, attributes: [NSAttributedString.Key: Any] = [:], in context: CGContext) {

    // fix for links with custom colour -- UIKit has trouble with this!
    if attributes[.link] != nil, let colorAttr = attributes[.foregroundColor] as? UIColor {
      context.setFillColor(colorAttr.cgColor)
    }

    super.showCGGlyphs(glyphs, positions: positions, count: glyphCount, font: font, textMatrix: textMatrix, attributes: attributes, in: context)
  }
}


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

class LayoutManagerDelegate: NSObject, NSLayoutManagerDelegate {
  func layoutManager(
    _ layoutManager: NSLayoutManager,
    shouldGenerateGlyphs glyphs: UnsafePointer<CGGlyph>,
    properties: UnsafePointer<NSLayoutManager.GlyphProperty>,
    characterIndexes: UnsafePointer<Int>,
    font: UIFont,
    forGlyphRange glyphRange: NSRange
  ) -> Int {

    guard let textStorage = layoutManager.textStorage else {
      fatalError()
    }

    let incomingGlyphsLength = glyphRange.length
    let firstCharIndex = characterIndexes[0]
    let lastCharIndex = characterIndexes[glyphRange.length - 1]
    let charactersRange = NSRange(location: firstCharIndex, length: lastCharIndex - firstCharIndex + 1)

    var operationRanges: [(range: NSRange, operation: TextTransform)] = []
    var hasOperations = false

    textStorage.enumerateAttribute(.textTransform, in: charactersRange, options: []) { attributeValue, range, _ in
      let transform = TextTransform(rawValue: attributeValue as? String ?? TextTransform.none.rawValue) ?? .none
      operationRanges.append((range: range, operation: transform))
      if transform != .none {
        hasOperations = true
      }
    }

    // bail if no operations. Returning 0 tells NSLayoutManager to use its default implementation
    if hasOperations == false {
      return 0
    }

    var operationResults: [(glyphs: [CGGlyph], properties: [NSLayoutManager.GlyphProperty], characterIndexes: [Int])] = []
    var bufferLength = 0
    var locationWithinIncomingGlyphsRange = 0

    let textStorageString = textStorage.string as NSString
    let ctFont = font as CTFont

    for operationRange in operationRanges {
      // derive the end location for the current string range in terms of the passed in glyph range
      var glyphSubrangeEnd = locationWithinIncomingGlyphsRange + operationRange.range.length // start the search here, it can't be less than that
      while glyphSubrangeEnd <= incomingGlyphsLength {
        let nextCharIndex = characterIndexes[glyphSubrangeEnd + 1]
        if !operationRange.range.contains(nextCharIndex) {
          break
        }
        glyphSubrangeEnd += 1
      }
      let glyphSubrangeLength = glyphSubrangeEnd - locationWithinIncomingGlyphsRange

      if operationRange.operation == .none {
        // copy the original glyphs from the input to this method
        let newGlyphs = Array(UnsafeBufferPointer(start: glyphs + locationWithinIncomingGlyphsRange, count: glyphSubrangeLength))
        let newProperties = Array(UnsafeBufferPointer(start: properties + locationWithinIncomingGlyphsRange, count: glyphSubrangeLength))
        let newCharIndexes = Array(UnsafeBufferPointer(start: characterIndexes + locationWithinIncomingGlyphsRange, count: glyphSubrangeLength))

        operationResults.append((glyphs: newGlyphs, properties: newProperties, characterIndexes: newCharIndexes))
        bufferLength += glyphSubrangeLength
      } else {
        // We now have a transform to do. Do it one character at a time in order to keep our character mapping accurate.
        textStorageString.enumerateSubstrings(in: operationRange.range, options: .byComposedCharacterSequences) { substring, substringRange, enclosingRange, _ in
          guard let substring else {
            return
          }

          // first check if we are one half of a composed character
          let composedNormalisedRange = textStorageString.rangeOfComposedCharacterSequence(at: substringRange.location)
          if composedNormalisedRange != substringRange {
            // for this case, we can't upper or lower case _half_ a character.
            operationResults.append((glyphs: [CGGlyph](repeating: CGGlyph(0), count: substringRange.length),
                                     properties: [NSLayoutManager.GlyphProperty](repeating: .null, count: substringRange.length),
                                     characterIndexes: Array(substringRange.location...(substringRange.location + substringRange.length))))
            bufferLength += substringRange.length
            return
          }

          // upper case the character
          let modifiedSubstring = operationRange.operation == .lowercase ? substring.lowercased() : substring.uppercased()

          // iterate through this _new_ string, in case upper casing it resulted in more than one composed character
          (modifiedSubstring as NSString).enumerateSubstrings(in: NSRange(location: 0, length: modifiedSubstring.lengthAsNSString()),
                                                              options: .byComposedCharacterSequences) { innerSubstring, innerSubstringRange, innerEnclosingRange, _ in
            guard let innerSubstring else {
              return
            }

            // Generate glyphs for the character
            let utf16 = Array(innerSubstring.utf16)
            var newGlyphs = [CGGlyph](repeating: 0, count: utf16.count)
            CTFontGetGlyphsForCharacters(ctFont, utf16, &newGlyphs, utf16.count) // if failure, glyph array will be empty as desired

            // build up our best guess at the glyph properties!
            var newProperties = [NSLayoutManager.GlyphProperty](repeating: .init(rawValue: 0), count: utf16.count)
            if let firstChar = innerSubstring.first, firstChar.isWhitespace {
              // checking the first char for whitespace is a reasonable approximation, because we don't expect there to be more than one character here
              newProperties = [NSLayoutManager.GlyphProperty](repeating: .elastic, count: utf16.count)
            }
            if utf16.count > 1 {
              for i in 1..<utf16.count {
                // Since we're expecting one character here (even in terms of our modified capitalised string), any extra points will be non base characters.
                newProperties[i] = .nonBaseCharacter
              }
            }

            // fill in character indexes incrementing based on substringRange (which is in terms of original string locations).
            var newCharIndexes = [Int](repeating: 0, count: newGlyphs.count)
            for i in 0..<substringRange.length {
              newCharIndexes[i] = i + substringRange.location
            }
            // If we have extra glyphs, repeat the last character index.
            if substringRange.length < newGlyphs.count {
              for i in substringRange.length..<newGlyphs.count {
                newCharIndexes[i] = substringRange.upperBound
              }
            }

            operationResults.append((glyphs: newGlyphs, properties: newProperties, characterIndexes: newCharIndexes))
            bufferLength += newGlyphs.count
          }
        }
      }
      locationWithinIncomingGlyphsRange += glyphSubrangeLength
    }

    let sumGlyphs = operationResults.flatMap { $0.glyphs }
    let sumProps = operationResults.flatMap { $0.properties }
    let sumCharacterIndexes = operationResults.flatMap { $0.characterIndexes }

    var fail = false
    sumGlyphs.withUnsafeBufferPointer { sumGlyphsBuffer in
      sumProps.withUnsafeBufferPointer { sumPropsBuffer in
        sumCharacterIndexes.withUnsafeBufferPointer { sumCharsBuffer in
          guard let sumGlyphsBaseAddress = sumGlyphsBuffer.baseAddress,
                let sumPropsBaseAddress = sumPropsBuffer.baseAddress,
                let sumCharsBaseAddress = sumCharsBuffer.baseAddress else {
            fail = true
            return
          }
          layoutManager.setGlyphs(sumGlyphsBaseAddress, properties: sumPropsBaseAddress, characterIndexes: sumCharsBaseAddress, font: font, forGlyphRange: NSRange(location: glyphRange.location, length: bufferLength))
        }
      }
    }

    return fail == true ? 0 : bufferLength
  }
}


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

/* The range cache is used when updating the NSTextStorage following a change to Lexical's
 * data model. In order to do the update with nuance, changing only the bits of the string
 * that have changed, we need to know where those bits of string are. The range cache
 * is how this information is stored, to save having to regenerate it (an expensive operation).
 */

struct RangeCacheItem {
  var location: Int = 0
  var preambleLength: Int = 0
  var childrenLength: Int = 0
  var textLength: Int = 0
  var postambleLength: Int = 0

  var range: NSRange {
    get {
      NSRange(location: location, length: preambleLength + childrenLength + textLength + postambleLength)
    }
  }
}

// MARK: - Search for nodes based on range

/*
 * This method is used to search a combination of the node tree and the range cache, to find a Point for a given
 * string location. The string location can be 0 <= x <= length. Location is specified in UTF16 code points, as
 * used by NSString. (Note that Swift string locations are not compatible.)
 *
 * If the string location falls in an invalid place (such as inside a multi-character preamble), this method
 * will return nil.
 *
 * searchDirection is used to break ties for when there would be more than one valid Point for a location. For
 * example, if the location sits between two consecutive Text nodes, the Point could either be at the end of the
 * first Text node, or at the start of the second Text node.
 */
internal func pointAtStringLocation(_ location: Int, searchDirection: UITextStorageDirection, rangeCache: [NodeKey: RangeCacheItem]) throws -> Point? {
  do {
    let searchResult = try evaluateNode(kRootNodeKey, stringLocation: location, searchDirection: searchDirection, rangeCache: rangeCache)
    guard let searchResult, let offset = searchResult.offset else {
      return nil
    }

    switch searchResult.type {
    case .endBoundary, .startBoundary, .illegal:
      throw LexicalError.internal("Failed to find node for location: \(location)")
    case .text:
      return Point(key: searchResult.nodeKey, offset: offset, type: .text)
    case .element:
      return Point(key: searchResult.nodeKey, offset: offset, type: .element)
    }
  } catch LexicalError.rangeCacheSearch {
    return nil
  }
}

private func evaluateNode(_ nodeKey: NodeKey, stringLocation: Int, searchDirection: UITextStorageDirection, rangeCache: [NodeKey: RangeCacheItem]) throws -> RangeCacheSearchResult? {
  guard let rangeCacheItem = rangeCache[nodeKey], let node = getNodeByKey(key: nodeKey) else {
    throw LexicalError.rangeCacheSearch("Couldn't find node or range cache item for key \(nodeKey)")
  }

  if !rangeCacheItem.entireRange().byAddingOne().contains(stringLocation) {
    return nil
  }

  if node is TextNode {
    let expandedTextRange = rangeCacheItem.textRange().byAddingOne()
    if expandedTextRange.contains(stringLocation) {
      return RangeCacheSearchResult(nodeKey: nodeKey, type: .text, offset: stringLocation - expandedTextRange.location)
    }
  }

  if let node = node as? ElementNode {
    let childrenArray = (searchDirection == .forward) ? node.getChildrenKeys() : node.getChildrenKeys().reversed()

    var possibleBoundaryElementResult: RangeCacheSearchResult?
    for childKey in childrenArray {
      // note: I'm using try? because that lets us attempt to still return a selection even if there's an error deeper in the tree.
      // This might be a mistake, in which case we can change it to just `try` and propagate the exception. @amyworrall
      guard let result = try? evaluateNode(childKey, stringLocation: stringLocation, searchDirection: searchDirection, rangeCache: rangeCache) else { continue }
      if result.type == .text || result.type == .element {
        return result
      }
      guard let childIndex = node.getChildrenKeys().firstIndex(of: childKey) else { continue }
      if result.type == .startBoundary {
        // the boundary of a child, so return self key with appropriate offset
        possibleBoundaryElementResult = RangeCacheSearchResult(nodeKey: nodeKey, type: .element, offset: childIndex)
      }
      if result.type == .endBoundary {
        possibleBoundaryElementResult = RangeCacheSearchResult(nodeKey: nodeKey, type: .element, offset: childIndex + 1)
      }
    }
    if let possibleBoundaryElementResult {
      // We do this 'possible result' check so that we prioritise text results where we can.
      return possibleBoundaryElementResult
    }
  }

  if rangeCacheItem.entireRange().length == 0 {
    // caret is at the last row - element with no children
    if stringLocation == rangeCacheItem.location {
      return RangeCacheSearchResult(nodeKey: nodeKey, type: .element, offset: 0)
    }

    // return the appropriate boundary for the search direction!
    let boundary: RangeCacheSearchResultType = (searchDirection == .forward) ? .startBoundary : .endBoundary
    return RangeCacheSearchResult(nodeKey: nodeKey, type: boundary, offset: nil)
  }

  if stringLocation == rangeCacheItem.location {
    if rangeCacheItem.preambleLength == 0 && node is ElementNode {
      return RangeCacheSearchResult(nodeKey: nodeKey, type: .element, offset: 0)
    }

    return RangeCacheSearchResult(nodeKey: nodeKey, type: .startBoundary, offset: nil)
  }

  if stringLocation == rangeCacheItem.entireRange().upperBound {
    return RangeCacheSearchResult(nodeKey: nodeKey, type: .endBoundary, offset: nil)
  }

  return RangeCacheSearchResult(nodeKey: nodeKey, type: .illegal, offset: nil)
}

fileprivate extension NSRange {
  func byAddingOne() -> NSRange {
    return NSRange(location: location, length: length + 1)
  }
}

internal extension RangeCacheItem {
  func entireRange() -> NSRange {
    return NSRange(location: location, length: preambleLength + childrenLength + textLength + postambleLength)
  }
  func textRange() -> NSRange {
    return NSRange(location: location + preambleLength + childrenLength, length: textLength)
  }
  func childrenRange() -> NSRange {
    return NSRange(location: location + preambleLength, length: childrenLength)
  }
}

private struct RangeCacheSearchResult {
  let nodeKey: NodeKey
  let type: RangeCacheSearchResultType
  let offset: Int?
}

private enum RangeCacheSearchResultType {
  case startBoundary // the boundary types are converted to element type for the parent element
  case endBoundary
  case text
  case element
  case illegal // used for if the search is inside a multi-character preamble/postamble
}

internal func updateRangeCacheForTextChange(nodeKey: NodeKey, delta: Int) {
  guard let editor = getActiveEditor(), let node = getNodeByKey(key: nodeKey) as? TextNode else {
    fatalError()
  }

  editor.rangeCache[nodeKey]?.textLength = node.getTextPart().lengthAsNSString()
  let parentKeys = node.getParents().map { $0.getKey() }

  for parentKey in parentKeys {
    editor.rangeCache[parentKey]?.childrenLength += delta
  }

  updateNodeLocationFor(nodeKey: kRootNodeKey, nodeIsAfterChangedNode: false, changedNodeKey: nodeKey, changedNodeParents: parentKeys, delta: delta)
}

private func updateNodeLocationFor(nodeKey: NodeKey, nodeIsAfterChangedNode: Bool, changedNodeKey: NodeKey, changedNodeParents: [NodeKey], delta: Int) {
  guard let editor = getActiveEditor() else {
    fatalError()
  }

  if nodeIsAfterChangedNode {
    editor.rangeCache[nodeKey]?.location += delta
  }

  var isAfterChangedNode = nodeIsAfterChangedNode

  if let elementNode = getNodeByKey(key: nodeKey) as? ElementNode, (isAfterChangedNode || changedNodeParents.contains(nodeKey)) {
    for child in elementNode.getChildren() {
      updateNodeLocationFor(nodeKey: child.getKey(), nodeIsAfterChangedNode: isAfterChangedNode, changedNodeKey: changedNodeKey, changedNodeParents: changedNodeParents, delta: delta)
      if child.getKey() == changedNodeKey || changedNodeParents.contains(child.getKey()) {
        isAfterChangedNode = true
      }
    }
  }
}


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

import UIKit

public class TextAttachment: NSTextAttachment {
  public var key: NodeKey?
  weak var editor: Editor?

  override public func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment: CGRect, glyphPosition _: CGPoint, characterIndex: Int) -> CGRect {
    guard let key, let editor else {
      return CGRect.zero
    }

    let attributes = textContainer?.layoutManager?.textStorage?.attributes(at: characterIndex, effectiveRange: nil) ?? [:]

    var bounds = CGRect.zero
    try? editor.read {
      guard let decoratorNode = getNodeByKey(key: key) as? DecoratorNode else {
        return
      }
      let size = decoratorNode.sizeForDecoratorView(textViewWidth: editor.frontend?.textLayoutWidth ?? CGFloat(0), attributes: attributes)
      bounds = CGRect(x: 0, y: 0, width: size.width, height: size.height)
    }

    self.bounds = bounds // cache the value so that our LayoutManager can pull it back out later
    return bounds
  }

  // necessary to stop UIKit drawing a placeholder image
  override public func image(forBounds imageBounds: CGRect, textContainer: NSTextContainer?, characterIndex charIndex: Int) -> UIImage? {
    return UIImage()
  }
}