import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TextFormat
import TelegramPresentationData
import InvisibleInkDustNode
import EmojiTextAttachmentView
import AccountContext
import TextFormat
import ChatTextLinkEditUI

public final class EmptyInputView: UIView, UIInputViewAudioFeedback {
    public var enableInputClicksWhenVisible: Bool {
        return true
    }
}

public final class TextFieldComponent: Component {
    public final class ExternalState {
        public fileprivate(set) var isEditing: Bool = false
        public fileprivate(set) var hasText: Bool = false
        public var initialText: NSAttributedString?
        
        public init() {
        }
    }
    
    public final class AnimationHint {
        public enum Kind {
            case textChanged
            case textFocusChanged
        }
        
        public let kind: Kind
        
        fileprivate init(kind: Kind) {
            self.kind = kind
        }
    }
    
    public let context: AccountContext
    public let strings: PresentationStrings
    public let externalState: ExternalState
    public let fontSize: CGFloat
    public let textColor: UIColor
    public let insets: UIEdgeInsets
    public let hideKeyboard: Bool
    public let present: (ViewController) -> Void
    
    public init(
        context: AccountContext,
        strings: PresentationStrings,
        externalState: ExternalState,
        fontSize: CGFloat,
        textColor: UIColor,
        insets: UIEdgeInsets,
        hideKeyboard: Bool,
        present: @escaping (ViewController) -> Void
    ) {
        self.context = context
        self.strings = strings
        self.externalState = externalState
        self.fontSize = fontSize
        self.textColor = textColor
        self.insets = insets
        self.hideKeyboard = hideKeyboard
        self.present = present
    }
    
    public static func ==(lhs: TextFieldComponent, rhs: TextFieldComponent) -> Bool {
        if lhs.strings !== rhs.strings {
            return false
        }
        if lhs.externalState !== rhs.externalState {
            return false
        }
        if lhs.fontSize != rhs.fontSize {
            return false
        }
        if lhs.textColor != rhs.textColor {
            return false
        }
        if lhs.insets != rhs.insets {
            return false
        }
        if lhs.hideKeyboard != rhs.hideKeyboard {
            return false
        }
        return true
    }
    
    public struct InputState {
        public var inputText: NSAttributedString
        public var selectionRange: Range<Int>
        
        public init(inputText: NSAttributedString, selectionRange: Range<Int>) {
            self.inputText = inputText
            self.selectionRange = selectionRange
        }
        
        public init(inputText: NSAttributedString) {
            self.inputText = inputText
            let length = inputText.length
            self.selectionRange = length ..< length
        }
    }
    
    public final class View: UIView, UITextViewDelegate, UIScrollViewDelegate {
        private let textContainer: NSTextContainer
        private let textStorage: NSTextStorage
        private let layoutManager: NSLayoutManager
        private let textView: UITextView
        
        private var spoilerView: InvisibleInkDustView?
        private var customEmojiContainerView: CustomEmojiContainerView?
        private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
        
        private var inputState: InputState {
            let selectionRange: Range<Int> = self.textView.selectedRange.location ..< (self.textView.selectedRange.location + self.textView.selectedRange.length)
            return InputState(inputText: stateAttributedStringForText(self.textView.attributedText ?? NSAttributedString()), selectionRange: selectionRange)
        }
        
        private var component: TextFieldComponent?
        private weak var state: EmptyComponentState?
        
        override init(frame: CGRect) {
            self.textContainer = NSTextContainer(size: CGSize())
            self.textContainer.widthTracksTextView = false
            self.textContainer.heightTracksTextView = false
            self.textContainer.lineBreakMode = .byWordWrapping
            self.textContainer.lineFragmentPadding = 8.0
            
            self.textStorage = NSTextStorage()

            self.layoutManager = NSLayoutManager()
            self.layoutManager.allowsNonContiguousLayout = false
            self.layoutManager.addTextContainer(self.textContainer)
            self.textStorage.addLayoutManager(self.layoutManager)
            
            self.textView = UITextView(frame: CGRect(), textContainer: self.textContainer)
            self.textView.translatesAutoresizingMaskIntoConstraints = false
            self.textView.backgroundColor = nil
            self.textView.layer.isOpaque = false
            self.textView.keyboardAppearance = .dark
            self.textView.indicatorStyle = .white
            self.textView.scrollIndicatorInsets = UIEdgeInsets(top: 9.0, left: 0.0, bottom: 9.0, right: 0.0)
            
            super.init(frame: frame)
            
            self.clipsToBounds = true
            
            self.textView.delegate = self
            self.addSubview(self.textView)
            
            self.textContainer.widthTracksTextView = false
            self.textContainer.heightTracksTextView = false
            
            self.textView.typingAttributes = [
                NSAttributedString.Key.font: Font.regular(17.0),
                NSAttributedString.Key.foregroundColor: UIColor.white
            ]
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        private func updateInputState(_ f: (InputState) -> InputState) {
            guard let component = self.component else {
                return
            }
            
            let inputState = f(self.inputState)
            
            self.textView.attributedText = textAttributedStringForStateText(inputState.inputText, fontSize: component.fontSize, textColor: component.textColor, accentTextColor: component.textColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
            self.textView.selectedRange = NSMakeRange(inputState.selectionRange.lowerBound, inputState.selectionRange.count)
            
            refreshChatTextInputAttributes(textView: self.textView, primaryTextColor: component.textColor, accentTextColor: component.textColor, baseFontSize: component.fontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
            
            self.updateEntities()
        }
        
        public func insertText(_ text: NSAttributedString) {
            self.updateInputState { state in
                return state.insertText(text)
            }
            self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
        }
        
        public func deleteBackward() {
            self.textView.deleteBackward()
        }
        
        public func updateText(_ text: NSAttributedString, selectionRange: Range<Int>) {
            self.updateInputState { _ in
                return TextFieldComponent.InputState(inputText: text, selectionRange: selectionRange)
            }
            self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
        }
        
        public func textViewDidChange(_ textView: UITextView) {
            guard let component = self.component else {
                return
            }
            refreshChatTextInputAttributes(textView: self.textView, primaryTextColor: component.textColor, accentTextColor: component.textColor, baseFontSize: component.fontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
            refreshChatTextInputTypingAttributes(self.textView, textColor: component.textColor, baseFontSize: component.fontSize)
            
            if self.spoilerIsDisappearing {
                self.spoilerIsDisappearing = false
                self.updateInternalSpoilersRevealed(false, animated: false)
            }
            
            self.updateEntities()
            
            self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
        }
        
        public func textViewDidChangeSelection(_ textView: UITextView) {
            guard let _ = self.component else {
                return
            }
            
            self.updateSpoilersRevealed()
        }
        
        public func textViewDidBeginEditing(_ textView: UITextView) {
            self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring)).withUserData(AnimationHint(kind: .textFocusChanged)))
        }
        
        public func textViewDidEndEditing(_ textView: UITextView) {
            self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring)).withUserData(AnimationHint(kind: .textFocusChanged)))
        }
        
        @available(iOS 16.0, *)
        public func textView(_ textView: UITextView, editMenuForTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? {
            let filteredActions: Set<String> = Set([
                "com.apple.menu.format",
                "com.apple.menu.replace"
            ])
            let suggestedActions = suggestedActions.filter {
                if let action = $0 as? UIMenu, filteredActions.contains(action.identifier.rawValue) {
                    return false
                } else {
                    return true
                }
            }
            guard let component = self.component, !textView.attributedText.string.isEmpty && textView.selectedRange.length > 0 else {
                return UIMenu(children: suggestedActions)
            }
                        
            let strings = component.strings
            var actions: [UIAction] = [
                UIAction(title: strings.TextFormat_Bold, image: nil) { [weak self] action in
                    if let self {
                        self.toggleAttribute(key: ChatTextInputAttributes.bold)
                    }
                },
                UIAction(title: strings.TextFormat_Italic, image: nil) { [weak self] action in
                    if let self {
                        self.toggleAttribute(key: ChatTextInputAttributes.italic)
                    }
                },
                UIAction(title: strings.TextFormat_Monospace, image: nil) { [weak self] action in
                    if let self {
                        self.toggleAttribute(key: ChatTextInputAttributes.monospace)
                    }
                },
                UIAction(title: strings.TextFormat_Link, image: nil) { [weak self] action in
                    if let self {
                        self.openLinkEditing()
                    }
                },
                UIAction(title: strings.TextFormat_Strikethrough, image: nil) { [weak self] action in
                    if let self {
                        self.toggleAttribute(key: ChatTextInputAttributes.strikethrough)
                    }
                },
                UIAction(title: strings.TextFormat_Underline, image: nil) { [weak self] action in
                    if let self {
                        self.toggleAttribute(key: ChatTextInputAttributes.underline)
                    }
                }
            ]
            actions.append(UIAction(title: strings.TextFormat_Spoiler, image: nil) { [weak self] action in
                if let self {
                    var animated = false
                    let attributedText = self.inputState.inputText
                    attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, _, _ in
                        if let _ = attributes[ChatTextInputAttributes.spoiler] {
                            animated = true
                        }
                    })
                    
                    self.toggleAttribute(key: ChatTextInputAttributes.spoiler)
                    
                    self.updateSpoilersRevealed(animated: animated)
                }
            })
            
            var updatedActions = suggestedActions
            let formatMenu = UIMenu(title: strings.TextFormat_Format, image: nil, children: actions)
            updatedActions.insert(formatMenu, at: 1)
            
            return UIMenu(children: updatedActions)
        }
        
        private func toggleAttribute(key: NSAttributedString.Key) {
            self.updateInputState { state in
                return state.addFormattingAttribute(attribute: key)
            }
        }
                
        private func openLinkEditing() {
            guard let component = self.component else {
                return
            }
            let selectionRange = self.inputState.selectionRange
            let text = self.inputState.inputText.attributedSubstring(from: NSRange(location: selectionRange.startIndex, length: selectionRange.count))
            var link: String?
            text.enumerateAttributes(in: NSMakeRange(0, text.length)) { attributes, _, _ in
                if let linkAttribute = attributes[ChatTextInputAttributes.textUrl] as? ChatTextInputTextUrlAttribute {
                    link = linkAttribute.url
                }
            }
            
            let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme)
            let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>) = (presentationData, .single(presentationData))
            let controller = chatTextLinkEditController(sharedContext: component.context.sharedContext, updatedPresentationData: updatedPresentationData, account: component.context.account, text: text.string, link: link, apply: { [weak self] link in
                if let self {
                    if let link = link {
                        self.updateInputState { state in
                            return state.addLinkAttribute(selectionRange: selectionRange, url: link)
                        }
                        self.textView.becomeFirstResponder()
                    }
//                    strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, {
//                        return $0.updatedInputMode({ _ in return inputMode }).updatedInterfaceState({
//                            $0.withUpdatedEffectiveInputState(ChatTextInputState(inputText: $0.effectiveInputState.inputText, selectionRange: selectionRange.endIndex ..< selectionRange.endIndex))
//                        })
//                    })
                }
            })
            component.present(controller)
        }
        
        public func scrollViewDidScroll(_ scrollView: UIScrollView) {
            //print("didScroll \(scrollView.bounds)")
        }
        
        public func getInputState() -> TextFieldComponent.InputState {
            return self.inputState
        }
        
        public func getAttributedText() -> NSAttributedString {
            Keyboard.applyAutocorrection(textView: self.textView)
            return self.inputState.inputText
        }
        
        public func setAttributedText(_ string: NSAttributedString) {
            self.updateInputState { _ in
                return TextFieldComponent.InputState(inputText: string, selectionRange: string.length ..< string.length)
            }
            self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
        }
        
        public func activateInput() {
            self.textView.becomeFirstResponder()
        }
        
        private var spoilersRevealed = false
        private var spoilerIsDisappearing = false
        private func updateSpoilersRevealed(animated: Bool = true) {
            let selectionRange = self.textView.selectedRange
            
            var revealed = false
            if let attributedText = self.textView.attributedText {
                attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in
                    if let _ = attributes[ChatTextInputAttributes.spoiler] {
                        if let _ = selectionRange.intersection(range) {
                            revealed = true
                        }
                    }
                })
            }
                
            guard self.spoilersRevealed != revealed else {
                return
            }
            self.spoilersRevealed = revealed
            
            if revealed {
                self.updateInternalSpoilersRevealed(true, animated: animated)
            } else {
                self.spoilerIsDisappearing = true
                Queue.mainQueue().after(1.5, {
                    self.updateInternalSpoilersRevealed(false, animated: true)
                    self.spoilerIsDisappearing = false
                })
            }
        }
        
        private func updateInternalSpoilersRevealed(_ revealed: Bool, animated: Bool) {
            guard let component = self.component, self.spoilersRevealed == revealed else {
                return
            }
            
            self.textView.isScrollEnabled = false
            
            refreshChatTextInputAttributes(textView: self.textView, primaryTextColor: component.textColor, accentTextColor: component.textColor, baseFontSize: component.fontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(component.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
            refreshChatTextInputTypingAttributes(self.textView, textColor: component.textColor, baseFontSize: component.fontSize)
            
            if self.textView.subviews.count > 1, animated {
                let containerView = self.textView.subviews[1]
                if let canvasView = containerView.subviews.first {
                    if let snapshotView = canvasView.snapshotView(afterScreenUpdates: false) {
                        snapshotView.frame = canvasView.frame.offsetBy(dx: 0.0, dy: -self.textView.contentOffset.y)
                        self.insertSubview(snapshotView, at: 0)
                        canvasView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
                        snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
                            self.textView.isScrollEnabled = false
                            snapshotView?.removeFromSuperview()
                            Queue.mainQueue().after(0.1) {
                                self.textView.isScrollEnabled = true
                            }
                        })
                    }
                }
            }
            Queue.mainQueue().after(0.1) {
                self.textView.isScrollEnabled = true
            }
            
            if let spoilerView = self.spoilerView {
                if animated {
                    let transition = Transition.easeInOut(duration: 0.3)
                    if revealed {
                        transition.setAlpha(view: spoilerView, alpha: 0.0)
                    } else {
                        transition.setAlpha(view: spoilerView, alpha: 1.0)
                    }
                } else {
                    spoilerView.alpha = revealed ? 0.0 : 1.0
                }
            }
        }
        
        func updateEntities() {
            guard let component = self.component else {
                return
            }

            var spoilerRects: [CGRect] = []
            var customEmojiRects: [(CGRect, ChatTextInputTextCustomEmojiAttribute)] = []

            let textView = self.textView
            if let attributedText = textView.attributedText {
                let beginning = textView.beginningOfDocument
                attributedText.enumerateAttributes(in: NSMakeRange(0, attributedText.length), options: [], using: { attributes, range, _ in
                    if let _ = attributes[ChatTextInputAttributes.spoiler] {
                        func addSpoiler(startIndex: Int, endIndex: Int) {
                            if let start = textView.position(from: beginning, offset: startIndex), let end = textView.position(from: start, offset: endIndex - startIndex), let textRange = textView.textRange(from: start, to: end) {
                                let textRects = textView.selectionRects(for: textRange)
                                for textRect in textRects {
                                    if textRect.rect.width > 1.0 && textRect.rect.size.height > 1.0 {
                                        spoilerRects.append(textRect.rect.insetBy(dx: 1.0, dy: 1.0).offsetBy(dx: 0.0, dy: 1.0))
                                    }
                                }
                            }
                        }
                        
                        var startIndex: Int?
                        var currentIndex: Int?
                        
                        let nsString = (attributedText.string as NSString)
                        nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in
                            if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil {
                                if let currentStartIndex = startIndex {
                                    startIndex = nil
                                    let endIndex = range.location
                                    addSpoiler(startIndex: currentStartIndex, endIndex: endIndex)
                                }
                            } else if startIndex == nil {
                                startIndex = range.location
                            }
                            currentIndex = range.location + range.length
                        }
                        
                        if let currentStartIndex = startIndex, let currentIndex = currentIndex {
                            startIndex = nil
                            let endIndex = currentIndex
                            addSpoiler(startIndex: currentStartIndex, endIndex: endIndex)
                        }
                    }
                    
                    if let value = attributes[ChatTextInputAttributes.customEmoji] as? ChatTextInputTextCustomEmojiAttribute {
                        if let start = textView.position(from: beginning, offset: range.location), let end = textView.position(from: start, offset: range.length), let textRange = textView.textRange(from: start, to: end) {
                            let textRects = textView.selectionRects(for: textRange)
                            for textRect in textRects {
                                customEmojiRects.append((textRect.rect, value))
                                break
                            }
                        }
                    }
                })
            }
            
            if !spoilerRects.isEmpty {
                let spoilerView: InvisibleInkDustView
                if let current = self.spoilerView {
                    spoilerView = current
                } else {
                    spoilerView = InvisibleInkDustView(textNode: nil, enableAnimations: component.context.sharedContext.energyUsageSettings.fullTranslucency)
                    spoilerView.alpha = self.spoilersRevealed ? 0.0 : 1.0
                    spoilerView.isUserInteractionEnabled = false
                    self.textView.addSubview(spoilerView)
                    self.spoilerView = spoilerView
                }
                spoilerView.frame = CGRect(origin: CGPoint(), size: self.textView.contentSize)
                spoilerView.update(size: self.textView.contentSize, color: component.textColor, textColor: component.textColor, rects: spoilerRects, wordRects: spoilerRects)
            } else if let spoilerView = self.spoilerView {
                spoilerView.removeFromSuperview()
                self.spoilerView = nil
            }

            if !customEmojiRects.isEmpty {
                let customEmojiContainerView: CustomEmojiContainerView
                if let current = self.customEmojiContainerView {
                    customEmojiContainerView = current
                } else {
                    customEmojiContainerView = CustomEmojiContainerView(emojiViewProvider: { [weak self] emoji in
                        guard let strongSelf = self, let emojiViewProvider = strongSelf.emojiViewProvider else {
                            return nil
                        }
                        return emojiViewProvider(emoji)
                    })
                    customEmojiContainerView.isUserInteractionEnabled = false
                    self.textView.addSubview(customEmojiContainerView)
                    self.customEmojiContainerView = customEmojiContainerView
                }

                customEmojiContainerView.update(fontSize: component.fontSize, textColor: component.textColor, emojiRects: customEmojiRects)
            } else if let customEmojiContainerView = self.customEmojiContainerView {
                customEmojiContainerView.removeFromSuperview()
                self.customEmojiContainerView = nil
            }
        }
        
        func update(component: TextFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
            self.component = component
            self.state = state
            
            if let initialText = component.externalState.initialText {
                component.externalState.initialText = nil
                self.updateInputState { _ in
                    return TextFieldComponent.InputState(inputText: initialText)
                }
            }
            
            if self.emojiViewProvider == nil {
                self.emojiViewProvider = { [weak self] emoji in
                    guard let component = self?.component else {
                        return UIView()
                    }
                    let pointSize = floor(24.0 * 1.3)
                    return EmojiTextAttachmentView(context: component.context, userLocation: .other, emoji: emoji, file: emoji.file, cache: component.context.animationCache, renderer: component.context.animationRenderer, placeholderColor: UIColor.white.withAlphaComponent(0.12), pointSize: CGSize(width: pointSize, height: pointSize))
                }
            }
            
            if self.textView.textContainerInset != component.insets {
                self.textView.textContainerInset = component.insets
            }
            self.textContainer.size = CGSize(width: availableSize.width - self.textView.textContainerInset.left - self.textView.textContainerInset.right, height: 10000000.0)
            self.layoutManager.ensureLayout(for: self.textContainer)
            
            let boundingRect = self.layoutManager.boundingRect(forGlyphRange: NSRange(location: 0, length: self.textStorage.length), in: self.textContainer)
            let size = CGSize(width: availableSize.width, height: min(availableSize.height, ceil(boundingRect.height) + self.textView.textContainerInset.top + self.textView.textContainerInset.bottom))
            
            let wasEditing = component.externalState.isEditing
            let isEditing = self.textView.isFirstResponder
            
            let refreshScrolling = self.textView.bounds.size != size
            self.textView.frame = CGRect(origin: CGPoint(), size: size)
            self.textView.panGestureRecognizer.isEnabled = isEditing
            
            if refreshScrolling {
                if isEditing {
                    if wasEditing {
                        self.textView.setContentOffset(CGPoint(x: 0.0, y: max(0.0, self.textView.contentSize.height - self.textView.bounds.height)), animated: false)
                    }
                } else {
                    self.textView.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: true)
                }
            }
            
            component.externalState.hasText = self.textStorage.length != 0
            component.externalState.isEditing = isEditing
            
            if component.hideKeyboard {
                if self.textView.inputView == nil {
                    self.textView.inputView = EmptyInputView()
                    if self.textView.isFirstResponder {
                        self.textView.reloadInputViews()
                    }
                }
            } else {
                if self.textView.inputView != nil {
                    self.textView.inputView = nil
                    if self.textView.isFirstResponder {
                        self.textView.reloadInputViews()
                    }
                }
            }
            
            self.updateEntities()
            
            return size
        }
    }
    
    public func makeView() -> View {
        return View(frame: CGRect())
    }
    
    public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
        return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
    }
}

extension TextFieldComponent.InputState {
    public func insertText(_ text: NSAttributedString) -> TextFieldComponent.InputState {
        let inputText = NSMutableAttributedString(attributedString: self.inputText)
        let range = self.selectionRange
        
        inputText.replaceCharacters(in: NSMakeRange(range.lowerBound, range.count), with: text)
        
        let selectionPosition = range.lowerBound + (text.string as NSString).length
        return TextFieldComponent.InputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition)
    }
    
    public func addFormattingAttribute(attribute: NSAttributedString.Key) -> TextFieldComponent.InputState {
        if !self.selectionRange.isEmpty {
            let nsRange = NSRange(location: self.selectionRange.lowerBound, length: self.selectionRange.count)
            var addAttribute = true
            var attributesToRemove: [NSAttributedString.Key] = []
            self.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, stop in
                for (key, _) in attributes {
                    if key == attribute && range == nsRange {
                        addAttribute = false
                        attributesToRemove.append(key)
                    }
                }
            }
            
            let result = NSMutableAttributedString(attributedString: self.inputText)
            for attribute in attributesToRemove {
                result.removeAttribute(attribute, range: nsRange)
            }
            if addAttribute {
                result.addAttribute(attribute, value: true as Bool, range: nsRange)
            }
            return TextFieldComponent.InputState(inputText: result, selectionRange: self.selectionRange)
        } else {
            return self
        }
    }

    public func clearFormattingAttributes() -> TextFieldComponent.InputState {
        if !self.selectionRange.isEmpty {
            let nsRange = NSRange(location: self.selectionRange.lowerBound, length: self.selectionRange.count)
            var attributesToRemove: [NSAttributedString.Key] = []
            self.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, stop in
                for (key, _) in attributes {
                    attributesToRemove.append(key)
                }
            }
            
            let result = NSMutableAttributedString(attributedString: self.inputText)
            for attribute in attributesToRemove {
                result.removeAttribute(attribute, range: nsRange)
            }
            return TextFieldComponent.InputState(inputText: result, selectionRange: self.selectionRange)
        } else {
            return self
        }
    }

    public func addLinkAttribute(selectionRange: Range<Int>, url: String) -> TextFieldComponent.InputState {
        if !selectionRange.isEmpty {
            let nsRange = NSRange(location: selectionRange.lowerBound, length: selectionRange.count)
            var linkRange = nsRange
            var attributesToRemove: [(NSAttributedString.Key, NSRange)] = []
            self.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, stop in
                for (key, _) in attributes {
                    if key == ChatTextInputAttributes.textUrl {
                        attributesToRemove.append((key, range))
                        linkRange = linkRange.union(range)
                    } else {
                        attributesToRemove.append((key, nsRange))
                    }
                }
            }

            let result = NSMutableAttributedString(attributedString: self.inputText)
            for (attribute, range) in attributesToRemove {
                result.removeAttribute(attribute, range: range)
            }
            result.addAttribute(ChatTextInputAttributes.textUrl, value: ChatTextInputTextUrlAttribute(url: url), range: nsRange)
            return TextFieldComponent.InputState(inputText: result, selectionRange: selectionRange)
        } else {
            return self
        }
    }
}