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

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 fileprivate(set) var textLength: Int = 0
        public var initialText: NSAttributedString?
        
        public var hasTrackingView = false
        
        public var currentEmojiSuggestion: EmojiSuggestion?
        public var dismissedEmojiSuggestionPosition: EmojiSuggestion.Position?
        
        public init() {
        }
    }
    
    public final class EmojiSuggestion {
        public struct Position: Equatable {
            public var range: NSRange
            public var value: String
        }
        
        public var localPosition: CGPoint
        public var position: Position
        public var disposable: Disposable?
        public var value: Any?
        
        init(localPosition: CGPoint, position: Position) {
            self.localPosition = localPosition
            self.position = position
            self.disposable = nil
            self.value = nil
        }
    }
    
    public enum PasteData {
        case sticker(image: UIImage, isMemoji: Bool)
        case images([UIImage])
        case video(Data)
        case gif(Data)
        case text
    }
    
    
    public final class AnimationHint {
        public enum Kind {
            case textChanged
            case textFocusChanged
        }
        
        public let kind: Kind
        
        public init(kind: Kind) {
            self.kind = kind
        }
    }
    
    public enum FormatMenuAvailability: Equatable {
        case available
        case locked
        case none
    }
    
    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 customInputView: UIView?
    public let resetText: NSAttributedString?
    public let isOneLineWhenUnfocused: Bool
    public let formatMenuAvailability: FormatMenuAvailability
    public let lockedFormatAction: () -> Void
    public let present: (ViewController) -> Void
    public let paste: (PasteData) -> Void
    
    public init(
        context: AccountContext,
        strings: PresentationStrings,
        externalState: ExternalState,
        fontSize: CGFloat,
        textColor: UIColor,
        insets: UIEdgeInsets,
        hideKeyboard: Bool,
        customInputView: UIView?,
        resetText: NSAttributedString?,
        isOneLineWhenUnfocused: Bool,
        formatMenuAvailability: FormatMenuAvailability,
        lockedFormatAction: @escaping () -> Void,
        present: @escaping (ViewController) -> Void,
        paste: @escaping (PasteData) -> Void
    ) {
        self.context = context
        self.strings = strings
        self.externalState = externalState
        self.fontSize = fontSize
        self.textColor = textColor
        self.insets = insets
        self.hideKeyboard = hideKeyboard
        self.customInputView = customInputView
        self.resetText = resetText
        self.isOneLineWhenUnfocused = isOneLineWhenUnfocused
        self.formatMenuAvailability = formatMenuAvailability
        self.lockedFormatAction = lockedFormatAction
        self.present = present
        self.paste = paste
    }
    
    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
        }
        if lhs.customInputView !== rhs.customInputView {
            return false
        }
        if lhs.resetText != rhs.resetText {
            return false
        }
        if lhs.isOneLineWhenUnfocused != rhs.isOneLineWhenUnfocused {
            return false
        }
        if lhs.formatMenuAvailability != rhs.formatMenuAvailability {
            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
        }
    }
    
    final class TextView: UITextView {
        var onPaste: () -> Bool = { return true }
        
        override func paste(_ sender: Any?) {
            if self.onPaste() {
                super.paste(sender)
            }
        }
        
        override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
            if action == #selector(self.paste(_:)) {
                return true
            }
            return super.canPerformAction(action, withSender: sender)
        }
    }
    
    public final class View: UIView, UITextViewDelegate, UIScrollViewDelegate {
        private let textContainer: NSTextContainer
        private let textStorage: NSTextStorage
        private let layoutManager: NSLayoutManager
        private let textView: TextView
        
        private var spoilerView: InvisibleInkDustView?
        private var customEmojiContainerView: CustomEmojiContainerView?
        private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
                
        private let ellipsisView = ComponentView<Empty>()
        
        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 = TextView(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
            
            if #available(iOS 13.0, *) {
                self.textView.overrideUserInterfaceStyle = .dark
            }
            
            self.textView.typingAttributes = [
                NSAttributedString.Key.font: Font.regular(17.0),
                NSAttributedString.Key.foregroundColor: UIColor.white
            ]
            
            self.textView.onPaste = { [weak self] in
                return self?.onPaste() ?? false
            }
        }
        
        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 hasFirstResponder() -> Bool {
            return self.textView.isFirstResponder
        }
        
        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)))
        }
        
        private func onPaste() -> Bool {
            guard let component = self.component else {
                return false
            }
            let pasteboard = UIPasteboard.general
                        
            var attributedString: NSAttributedString?
            if let data = pasteboard.data(forPasteboardType: kUTTypeRTF as String) {
                attributedString = chatInputStateStringFromRTF(data, type: NSAttributedString.DocumentType.rtf)
            } else if let data = pasteboard.data(forPasteboardType: "com.apple.flat-rtfd") {
                attributedString = chatInputStateStringFromRTF(data, type: NSAttributedString.DocumentType.rtfd)
            }
            
            if let attributedString = attributedString {
                self.updateInputState { current in
                    if let inputText = current.inputText.mutableCopy() as? NSMutableAttributedString {
                        inputText.replaceCharacters(in: NSMakeRange(current.selectionRange.lowerBound, current.selectionRange.count), with: attributedString)
                        let updatedRange = current.selectionRange.lowerBound + attributedString.length
                        return InputState(inputText: inputText, selectionRange: updatedRange ..< updatedRange)
                    } else {
                        return InputState(inputText: attributedString)
                    }
                }
                self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
                component.paste(.text)
                return false
            }
            
            if let data = pasteboard.data(forPasteboardType: "com.compuserve.gif") {
                component.paste(.gif(data))
                return false
            } else if let data = pasteboard.data(forPasteboardType: "public.mpeg-4") {
                component.paste(.video(data))
                return false
            } else {
                var images: [UIImage] = []
                var isPNG = false
                var isMemoji = false
                for item in pasteboard.items {
                    if let image = item["com.apple.png-sticker"] as? UIImage {
                        images.append(image)
                        isPNG = true
                        isMemoji = true
                    } else if let image = item[kUTTypePNG as String] as? UIImage {
                        images.append(image)
                        isPNG = true
                    } else if let image = item["com.apple.uikit.image"] as? UIImage {
                        images.append(image)
                        isPNG = true
                    } else if let image = item[kUTTypeJPEG as String] as? UIImage {
                        images.append(image)
                    } else if let image = item[kUTTypeGIF as String] as? UIImage {
                        images.append(image)
                    }
                }
                
                if isPNG && images.count == 1, let image = images.first, let cgImage = image.cgImage {
                    let maxSide = max(image.size.width, image.size.height)
                    if maxSide.isZero {
                        return false
                    }
                    let aspectRatio = min(image.size.width, image.size.height) / maxSide
                    if isMemoji || (imageHasTransparency(cgImage) && aspectRatio > 0.2) {
                        component.paste(.sticker(image: image, isMemoji: isMemoji))
                        return false
                    }
                }
                
                if !images.isEmpty {
                    component.paste(.images(images))
                    return false
                }
            }
            
            component.paste(.text)
            return true
        }
        
        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()
            self.updateEmojiSuggestion(transition: .immediate)
        }
        
        public func textViewDidBeginEditing(_ textView: UITextView) {
            guard let component = self.component else {
                return
            }
            self.state?.updated(transition: Transition(animation: .curve(duration: 0.5, curve: .spring)).withUserData(AnimationHint(kind: .textFocusChanged)))
            if component.isOneLineWhenUnfocused {
                Queue.mainQueue().justDispatch {
                    self.textView.selectedTextRange = self.textView.textRange(from: self.textView.endOfDocument, to: self.textView.endOfDocument)
                }
            }
        }
        
        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
            
            if case .none = component.formatMenuAvailability {
                return UIMenu(children: suggestedActions)
            }
            
            if case .locked = component.formatMenuAvailability {
                var updatedActions = suggestedActions
                let formatAction = UIAction(title: strings.TextFormat_Format, image: nil) { [weak self] action in
                    if let self {
                        self.component?.lockedFormatAction()
                    }
                }
                updatedActions.insert(formatAction, at: 1)
                return UIMenu(children: updatedActions)
            }
                        
            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()
                    }
                }
            })
            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, updateState: Bool) {
            self.updateInputState { _ in
                return TextFieldComponent.InputState(inputText: string, selectionRange: string.length ..< string.length)
            }
            if updateState {
                self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
            }
        }
        
        public func activateInput() {
            self.textView.becomeFirstResponder()
        }
        
        public func deactivateInput() {
            self.textView.resignFirstResponder()
        }
        
        public var isActive: Bool {
            return self.textView.isFirstResponder
        }
        
        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, CGFloat)] = []

            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) {
                            var emojiFontSize = component.fontSize
                            if let font = attributes[.font] as? UIFont {
                                emojiFontSize = font.pointSize
                            }
                            let textRects = textView.selectionRects(for: textRange)
                            for textRect in textRects {
                                customEmojiRects.append((textRect.rect, value, emojiFontSize))
                                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
            }
        }
        
        public func updateEmojiSuggestion(transition: Transition) {
            guard let component = self.component else {
                return
            }
                        
            var hasTracking = false
            var hasTrackingView = false
            if self.textView.selectedRange.length == 0 && self.textView.selectedRange.location > 0 {
                let selectedSubstring = self.textView.attributedText.attributedSubstring(from: NSRange(location: 0, length: self.textView.selectedRange.location))
                if let lastCharacter = selectedSubstring.string.last, String(lastCharacter).isSingleEmoji {
                    let queryLength = (String(lastCharacter) as NSString).length
                    if selectedSubstring.attribute(ChatTextInputAttributes.customEmoji, at: selectedSubstring.length - queryLength, effectiveRange: nil) == nil {
                        let beginning = self.textView.beginningOfDocument
                        
                        let characterRange = NSRange(location: selectedSubstring.length - queryLength, length: queryLength)
                        
                        let start = self.textView.position(from: beginning, offset: selectedSubstring.length - queryLength)
                        let end = self.textView.position(from: beginning, offset: selectedSubstring.length)
                        
                        if let start = start, let end = end, let textRange = self.textView.textRange(from: start, to: end) {
                            let selectionRects = self.textView.selectionRects(for: textRange)
                            let emojiSuggestionPosition = EmojiSuggestion.Position(range: characterRange, value: String(lastCharacter))
                            
                            hasTracking = true
                            
                            if let trackingRect = selectionRects.first?.rect {
                                let trackingPosition = CGPoint(x: trackingRect.midX, y: trackingRect.minY)
                                if component.externalState.dismissedEmojiSuggestionPosition == emojiSuggestionPosition {
                                } else {
                                    hasTrackingView = true
                                    
                                    let emojiSuggestion: EmojiSuggestion
                                    if let current = component.externalState.currentEmojiSuggestion, current.position.value == emojiSuggestionPosition.value {
                                        emojiSuggestion = current
                                    } else {

                                        emojiSuggestion = EmojiSuggestion(localPosition: trackingPosition, position: emojiSuggestionPosition)
                                        component.externalState.currentEmojiSuggestion = emojiSuggestion
                                    }
                                    emojiSuggestion.localPosition = trackingPosition
                                    emojiSuggestion.position = emojiSuggestionPosition
                                    component.externalState.dismissedEmojiSuggestionPosition = nil
                                }
                            }
                        }
                    }
                }
            }
            if !hasTracking {
                component.externalState.dismissedEmojiSuggestionPosition = nil
            }
            component.externalState.hasTrackingView = hasTrackingView
        }
        
        func rightmostPositionOfFirstLine() -> CGPoint? {
            let glyphRange = self.layoutManager.glyphRange(for: self.textContainer)
            
            if glyphRange.length == 0 { return nil }
                
            var lineRect = CGRect.zero
            var glyphIndexForStringStart = glyphRange.location
            var lineRange: NSRange = NSRange()
                
            repeat {
                lineRect = self.layoutManager.lineFragmentUsedRect(forGlyphAt: glyphIndexForStringStart, effectiveRange: &lineRange)
                if NSMaxRange(lineRange) > glyphRange.length {
                    lineRange.length = glyphRange.length - lineRange.location
                }
                glyphIndexForStringStart = NSMaxRange(lineRange)
            } while glyphIndexForStringStart < NSMaxRange(glyphRange) && !NSLocationInRange(glyphRange.location, lineRange)
                
            let padding = self.textView.textContainerInset.left
            var rightmostX = lineRect.maxX + padding
            let rightmostY = lineRect.minY + self.textView.textContainerInset.top
            
            let nsString = (self.textView.text as NSString)
            let firstLineEndRange = NSMakeRange(lineRange.location + lineRange.length - 1, 1)
            if nsString.length > firstLineEndRange.location + firstLineEndRange.length {
                let lastChar = nsString.substring(with: firstLineEndRange)
                if lastChar == " " {
                    rightmostX -= 2.0
                }
            }
            
            return CGPoint(x: rightmostX, y: rightmostY)
        }
        
        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)
                }
            } else if let resetText = component.resetText {
                self.updateInputState { _ in
                    return TextFieldComponent.InputState(inputText: resetText)
                }
            }
            
            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))
                }
            }
            
            let wasEditing = component.externalState.isEditing
            let isEditing = self.textView.isFirstResponder
            
            if self.textView.textContainerInset != component.insets {
                self.textView.textContainerInset = component.insets
            }
            
            var availableSize = availableSize
            if !isEditing && component.isOneLineWhenUnfocused {
                availableSize.width += 32.0
            }
            
            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))
            
            var refreshScrolling = self.textView.bounds.size != size
            if component.isOneLineWhenUnfocused && !isEditing && isEditing != wasEditing {
                refreshScrolling = true
            }
            self.textView.frame = CGRect(origin: CGPoint(), size: size)
            self.textView.panGestureRecognizer.isEnabled = isEditing
            
            self.updateEmojiSuggestion(transition: .immediate)
            
            if refreshScrolling {
                if isEditing {
                    if wasEditing || component.isOneLineWhenUnfocused {
                        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
            component.externalState.textLength = self.textStorage.string.count
            
            if let inputView = component.customInputView {
                if self.textView.inputView == nil {
                    self.textView.inputView = inputView
                    if self.textView.isFirstResponder {
                        // Avoid layout cycle
                        DispatchQueue.main.async { [weak self] in
                            self?.textView.reloadInputViews()
                        }
                    }
                }
            } else if component.hideKeyboard {
                if self.textView.inputView == nil {
                    self.textView.inputView = EmptyInputView()
                    if self.textView.isFirstResponder {
                        // Avoid layout cycle
                        DispatchQueue.main.async { [weak self] in
                            self?.textView.reloadInputViews()
                        }
                    }
                }
            } else {
                if self.textView.inputView != nil {
                    self.textView.inputView = nil
                    if self.textView.isFirstResponder {
                        // Avoid layout cycle
                        DispatchQueue.main.async { [weak self] in
                            self?.textView.reloadInputViews()
                        }
                    }
                }
            }
            
            if component.isOneLineWhenUnfocused, let position = self.rightmostPositionOfFirstLine() {
                let ellipsisSize = self.ellipsisView.update(
                    transition: transition,
                    component: AnyComponent(
                        Text(
                            text: "\u{2026}",
                            font: Font.regular(component.fontSize),
                            color: component.textColor
                        )
                    ),
                    environment: {},
                    containerSize: availableSize
                )
                if let view = self.ellipsisView.view {
                    if view.superview == nil {
                        view.alpha = 0.0
                        view.isUserInteractionEnabled = false
                        self.textView.addSubview(view)
                    }
                    let ellipsisFrame = CGRect(origin: CGPoint(x: position.x - 8.0, y: position.y), size: ellipsisSize)
                    transition.setFrame(view: view, frame: ellipsisFrame)
                    
                    let hasMoreThanOneLine = ellipsisFrame.maxY < self.textView.contentSize.height - 12.0
                    
                    let ellipsisTransition: Transition
                    if isEditing {
                        ellipsisTransition = .easeInOut(duration: 0.2)
                    } else {
                        ellipsisTransition = .easeInOut(duration: 0.3)
                    }
                    ellipsisTransition.setAlpha(view: view, alpha: isEditing || !hasMoreThanOneLine ? 0.0 : 1.0)
                }
            } else {
                if let view = self.ellipsisView.view {
                    view.removeFromSuperview()
                }
            }
            
            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
        }
    }
}