import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import AccountContext
import ChatPresentationInterfaceState
import ContextUI
import ChatInterfaceState
import PresentationDataUtils
import ChatMessageTextBubbleContentNode
import TextFormat
import ChatMessageItemView
import ChatMessageBubbleItemNode
import TelegramNotices
import ChatMessageWebpageBubbleContentNode
import PremiumUI
import UndoUI

private enum OptionsId: Hashable {
    case reply
    case forward
    case link
}

private func presentChatInputOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode, initialId: OptionsId) {
    var getContextController: (() -> ContextController?)?
    
    var sources: [ContextController.Source] = []
    
    let replySelectionState = Promise<ChatControllerSubject.MessageOptionsInfo.SelectionState>(ChatControllerSubject.MessageOptionsInfo.SelectionState(canQuote: false, quote: nil))
    
    if let source = chatReplyOptions(selfController: selfController, sourceNode: sourceNode, getContextController: {
        return getContextController?()
    }, selectionState: replySelectionState) {
        sources.append(source)
    }
    
    var forwardDismissedForCancel: (() -> Void)?
    if let (source, dismissedForCancel) = chatForwardOptions(selfController: selfController, sourceNode: sourceNode, getContextController: {
        return getContextController?()
    }) {
        forwardDismissedForCancel = dismissedForCancel
        sources.append(source)
    }
    
    if let source = chatLinkOptions(selfController: selfController, sourceNode: sourceNode, getContextController: {
        return getContextController?()
    }, replySelectionState: replySelectionState) {
        sources.append(source)
    }
    
    if sources.isEmpty {
        return
    }
    
    selfController.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()

    selfController.canReadHistory.set(false)
    
    let contextController = ContextController(
        presentationData: selfController.presentationData,
        configuration: ContextController.Configuration(
            sources: sources,
            initialId: AnyHashable(initialId)
        )
    )
    contextController.dismissed = { [weak selfController] in
        selfController?.canReadHistory.set(true)
    }
    
    getContextController = { [weak contextController] in
        return contextController
    }
    
    contextController.dismissedForCancel = {
        forwardDismissedForCancel?()
    }
    
    selfController.presentInGlobalOverlay(contextController)
}

private func chatForwardOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode, getContextController: @escaping () -> ContextController?) -> (ContextController.Source, () -> Void)? {
    guard let peerId = selfController.chatLocation.peerId else {
        return nil
    }
    guard let initialForwardMessageIds = selfController.presentationInterfaceState.interfaceState.forwardMessageIds, !initialForwardMessageIds.isEmpty else {
        return nil
    }
    let presentationData = selfController.presentationData
    
    let forwardOptions = selfController.presentationInterfaceStatePromise.get()
    |> map { state -> ChatControllerSubject.ForwardOptions in
        var hideNames = state.interfaceState.forwardOptionsState?.hideNames ?? false
        if peerId.namespace == Namespaces.Peer.SecretChat {
            hideNames = true
        }
        return ChatControllerSubject.ForwardOptions(hideNames: hideNames, hideCaptions: state.interfaceState.forwardOptionsState?.hideCaptions ?? false)
    }
    |> distinctUntilChanged
    
    let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [peerId], ids: selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], info: .forward(ChatControllerSubject.MessageOptionsInfo.Forward(options: forwardOptions))), botStart: nil, mode: .standard(.previewing))
    chatController.canReadHistory.set(false)
    
    let messageIds = selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? []
    let messagesCount: Signal<Int, NoError>
    if let chatController = chatController as? ChatControllerImpl, messageIds.count > 1 {
        messagesCount = .single(messageIds.count)
        |> then(
            chatController.presentationInterfaceStatePromise.get()
            |> map { state -> Int in
                return state.interfaceState.selectionState?.selectedIds.count ?? 1
            }
        )
    } else {
        messagesCount = .single(1)
    }
    
    let accountPeerId = selfController.context.account.peerId
    let items = combineLatest(forwardOptions, selfController.context.account.postbox.messagesAtIds(messageIds), messagesCount)
    |> deliverOnMainQueue
    |> map { [weak selfController] forwardOptions, messages, messagesCount -> [ContextMenuItem] in
        guard let selfController else {
            return []
        }
        var items: [ContextMenuItem] = []
        
        var hasCaptions = false
        var uniquePeerIds = Set<PeerId>()
        
        var hasOther = false
        var hasNotOwnMessages = false
        for message in messages {
            if let author = message.effectiveAuthor {
                if !uniquePeerIds.contains(author.id) {
                    uniquePeerIds.insert(author.id)
                }
                if message.id.peerId == accountPeerId && message.forwardInfo == nil {
                } else {
                    hasNotOwnMessages = true
                }
            }
            
            var isDice = false
            var isMusic = false
            for media in message.media {
                if let media = media as? TelegramMediaFile, media.isMusic {
                    isMusic = true
                } else if media is TelegramMediaDice {
                    isDice = true
                } else {
                    if !message.text.isEmpty {
                        if media is TelegramMediaImage || media is TelegramMediaFile {
                            hasCaptions = true
                        }
                    }
                }
            }
            if !isDice && !isMusic {
                hasOther = true
            }
        }
        
        var canHideNames = hasNotOwnMessages && hasOther
        if case let .peer(peerId) = selfController.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat {
            canHideNames = false
        }
        let hideNames = forwardOptions.hideNames
        let hideCaptions = forwardOptions.hideCaptions
        
        if canHideNames {
            items.append(.action(ContextMenuActionItem(text: hideNames ? (uniquePeerIds.count == 1 ? presentationData.strings.Conversation_ForwardOptions_ShowSendersName : presentationData.strings.Conversation_ForwardOptions_ShowSendersNames) : (uniquePeerIds.count == 1 ? presentationData.strings.Conversation_ForwardOptions_HideSendersName : presentationData.strings.Conversation_ForwardOptions_HideSendersNames), icon: { _ in
                return nil
            }, iconAnimation: ContextMenuActionItem.IconAnimation(
                name: !hideNames ? "message_preview_person_on" : "message_preview_person_off"
            ), action: { [weak selfController] _, f in
                selfController?.interfaceInteraction?.updateForwardOptionsState({ current in
                    var updated = current
                    if hideNames {
                        updated.hideNames = false
                        updated.hideCaptions = false
                        updated.unhideNamesOnCaptionChange = false
                    } else {
                        updated.hideNames = true
                        updated.unhideNamesOnCaptionChange = false
                    }
                    return updated
                })
            })))
        }
        
        if hasCaptions {
            items.append(.action(ContextMenuActionItem(text: hideCaptions ? presentationData.strings.Conversation_ForwardOptions_ShowCaption : presentationData.strings.Conversation_ForwardOptions_HideCaption, icon: { _ in
                return nil
            }, iconAnimation: ContextMenuActionItem.IconAnimation(
                name: !hideCaptions ? "message_preview_caption_off" : "message_preview_caption_on"
            ), action: { [weak selfController] _, f in
                selfController?.interfaceInteraction?.updateForwardOptionsState({ current in
                    var updated = current
                    if hideCaptions {
                        updated.hideCaptions = false
                        if canHideNames {
                            if updated.unhideNamesOnCaptionChange {
                                updated.unhideNamesOnCaptionChange = false
                                updated.hideNames = false
                            }
                        }
                    } else {
                        updated.hideCaptions = true
                        if canHideNames {
                            if !updated.hideNames {
                                updated.hideNames = true
                                updated.unhideNamesOnCaptionChange = true
                            }
                        }
                    }
                    return updated
                })
            })))
        }
        
        if !items.isEmpty {
            items.append(.separator)
        }
        
        items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ForwardOptions_ChangeRecipient, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replace"), color: theme.contextMenu.primaryColor) }, action: { [weak selfController] c, f in
            selfController?.interfaceInteraction?.forwardCurrentForwardMessages()
            
            f(.default)
        })))
        
        items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_MessageOptionsApplyChanges, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { _, f in
            f(.default)
        })))
        
        items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ForwardOptionsCancel, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak selfController] c, f in
            f(.default)
            
            guard let selfController else {
                return
            }
            selfController.updateChatPresentationInterfaceState(interactive: false, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(nil).withoutSelectionState() }) })
        })))
        
        return items
    }
    
    let dismissedForCancel: () -> Void = { [weak selfController, weak chatController] in
        guard let selfController else {
            return
        }
        if let selectedMessageIds = (chatController as? ChatControllerImpl)?.selectedMessageIds {
            var forwardMessageIds = selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? []
            forwardMessageIds = forwardMessageIds.filter { selectedMessageIds.contains($0) }
            selfController.updateChatPresentationInterfaceState(interactive: false, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(forwardMessageIds) }) })
        }
    }
    
    return (ContextController.Source(
        id: AnyHashable(OptionsId.forward),
        title: selfController.presentationData.strings.Conversation_MessageOptionsTabForward,
        source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)),
        items: items |> map { ContextController.Items(id: AnyHashable("forward"), content: .list($0)) }
    ), dismissedForCancel)
}

func presentChatForwardOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode) {
    presentChatInputOptions(selfController: selfController, sourceNode: sourceNode, initialId: .forward)
}

private func generateChatReplyOptionItems(selfController: ChatControllerImpl, chatController: ChatControllerImpl) -> Signal<ContextController.Items, NoError> {
    guard let replySubject = selfController.presentationInterfaceState.interfaceState.replyMessageSubject else {
        return .complete()
    }
    
    let applyCurrentQuoteSelection: () -> Void = { [weak selfController, weak chatController] in
        guard let selfController, let chatController else {
            return
        }
        var messageItemNode: ChatMessageItemView?
        chatController.chatDisplayNode.historyNode.enumerateItemNodes { itemNode in
            if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, item.message.id == replySubject.messageId {
                messageItemNode = itemNode
            }
            return true
        }
        var targetContentNode: ChatMessageTextBubbleContentNode?
        if let messageItemNode = messageItemNode as? ChatMessageBubbleItemNode {
            for contentNode in messageItemNode.contentNodes {
                if let contentNode = contentNode as? ChatMessageTextBubbleContentNode {
                    targetContentNode = contentNode
                    break
                }
            }
        }
        guard let contentNode = targetContentNode else {
            return
        }
        guard let textSelection = contentNode.getCurrentTextSelection() else {
            return
        }
        var quote: EngineMessageReplyQuote?
        let trimmedText = trimStringWithEntities(string: textSelection.text, entities: textSelection.entities, maxLength: quoteMaxLength(appConfig: selfController.context.currentAppConfiguration.with({ $0 })))
        if !trimmedText.string.isEmpty {
            quote = EngineMessageReplyQuote(text: trimmedText.string, offset: textSelection.offset, entities: trimmedText.entities, media: nil)
        }
        
        selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject(messageId: replySubject.messageId, quote: quote)).withoutSelectionState() }) })
    }
    
    let items = combineLatest(queue: .mainQueue(),
        selfController.context.account.postbox.messagesAtIds([replySubject.messageId]),
        ApplicationSpecificNotice.getReplyQuoteTextSelectionTips(accountManager: selfController.context.sharedContext.accountManager)
    )
    |> deliverOnMainQueue
    |> map { [weak selfController, weak chatController] messages, quoteTextSelectionTips -> ContextController.Items in
        guard let selfController, let chatController else {
            return ContextController.Items(content: .list([]))
        }
        
        var items: [ContextMenuItem] = []
        
        if replySubject.quote != nil {
            items.append(.action(ContextMenuActionItem(text: selfController.presentationData.strings.Conversation_MessageOptionsQuoteSelectedPart, icon: { theme in
                return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/QuoteSelected"), color: theme.contextMenu.primaryColor)
            }, action: { _, f in
                applyCurrentQuoteSelection()
                
                f(.default)
            })))
        } else if let message = messages.first, !message.text.isEmpty {
            items.append(.action(ContextMenuActionItem(text: selfController.presentationData.strings.Conversation_MessageOptionsQuoteSelect, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Quote"), color: theme.contextMenu.primaryColor) }, action: { [weak selfController, weak chatController] c, _ in
                guard let selfController, let chatController else {
                    return
                }
                var messageItemNode: ChatMessageItemView?
                chatController.chatDisplayNode.historyNode.enumerateItemNodes { itemNode in
                    if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, item.message.id == replySubject.messageId {
                        messageItemNode = itemNode
                    }
                    return true
                }
                if let messageItemNode = messageItemNode as? ChatMessageBubbleItemNode {
                    for contentNode in messageItemNode.contentNodes {
                        if let contentNode = contentNode as? ChatMessageTextBubbleContentNode {
                            contentNode.beginTextSelection(range: nil)
                            
                            var subItems: [ContextMenuItem] = []
                            
                            subItems.append(.action(ContextMenuActionItem(text: selfController.presentationData.strings.Common_Back, icon: { theme in
                                return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor)
                            }, iconPosition: .left, action: { c, _ in
                                c.popItems()
                            })))
                            subItems.append(.separator)
                            
                            subItems.append(.action(ContextMenuActionItem(text: selfController.presentationData.strings.Conversation_MessageOptionsQuoteSelectedPart, icon: { theme in
                                return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/QuoteSelected"), color: theme.contextMenu.primaryColor)
                            }, action: { [weak selfController, weak contentNode] _, f in
                                guard let selfController, let contentNode else {
                                    return
                                }
                                guard let textSelection = contentNode.getCurrentTextSelection() else {
                                    return
                                }
                                
                                var quote: EngineMessageReplyQuote?
                                let trimmedText = trimStringWithEntities(string: textSelection.text, entities: textSelection.entities, maxLength: quoteMaxLength(appConfig: selfController.context.currentAppConfiguration.with({ $0 })))
                                if !trimmedText.string.isEmpty {
                                    quote = EngineMessageReplyQuote(text: trimmedText.string, offset: textSelection.offset, entities: trimmedText.entities, media: nil)
                                }
                                
                                selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject(messageId: replySubject.messageId, quote: quote)).withoutSelectionState() }) })
                                
                                f(.default)
                            })))
                            
                            c.pushItems(items: .single(ContextController.Items(content: .list(subItems), dismissed: { [weak contentNode] in
                                guard let contentNode else {
                                    return
                                }
                                contentNode.cancelTextSelection()
                            })))
                            
                            break
                        }
                    }
                }
            })))
        }
        
        var canReplyInAnotherChat = true
        
        if let message = messages.first {
            if selfController.presentationInterfaceState.copyProtectionEnabled {
                canReplyInAnotherChat = false
            }
            
            var isAction = false
            for media in message.media {
                if media is TelegramMediaAction || media is TelegramMediaExpiredContent {
                    isAction = true
                } else if let story = media as? TelegramMediaStory {
                    if story.isMention {
                        isAction = true
                    }
                }
            }
            
            if isAction {
                canReplyInAnotherChat = false
            }
            if message.isCopyProtected() {
                canReplyInAnotherChat = false
            }
            if message.id.peerId.namespace == Namespaces.Peer.SecretChat {
                canReplyInAnotherChat = false
            }
            if message.minAutoremoveOrClearTimeout == viewOnceTimeout {
                canReplyInAnotherChat = false
            }
        }
        
        if canReplyInAnotherChat {
            items.append(.action(ContextMenuActionItem(text: selfController.presentationData.strings.Conversation_MessageOptionsReplyInAnotherChat, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replace"), color: theme.contextMenu.primaryColor) }, action: { [weak selfController] c, f in
                applyCurrentQuoteSelection()
                
                f(.default)
                
                guard let selfController else {
                    return
                }
                guard let replySubject = selfController.presentationInterfaceState.interfaceState.replyMessageSubject else {
                    return
                }
                moveReplyMessageToAnotherChat(selfController: selfController, replySubject: replySubject)
            })))
        }
        
        if !items.isEmpty {
            items.append(.separator)
            
            items.append(.action(ContextMenuActionItem(text: selfController.presentationData.strings.Conversation_MessageOptionsApplyChanges, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { _, f in
                applyCurrentQuoteSelection()
                
                f(.default)
            })))
        }
        
        if replySubject.quote != nil {
            items.append(.action(ContextMenuActionItem(text: selfController.presentationData.strings.Conversation_MessageOptionsQuoteRemove, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/QuoteRemove"), color: theme.contextMenu.destructiveColor) }, action: { [weak selfController] c, f in
                f(.default)
                
                guard let selfController else {
                    return
                }
                var replySubject = replySubject
                replySubject.quote = nil
                selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(replySubject).withoutSelectionState() }).updatedSearch(nil) })
            })))
        } else {
            items.append(.action(ContextMenuActionItem(text: selfController.presentationData.strings.Conversation_MessageOptionsReplyCancel, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak selfController] c, f in
                f(.default)
                
                guard let selfController else {
                    return
                }
                var replySubject = replySubject
                replySubject.quote = nil
                selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(nil).withoutSelectionState() }).updatedSearch(nil) })
            })))
        }
        
        var tip: ContextController.Tip?
        if quoteTextSelectionTips <= 3, let message = messages.first, !message.text.isEmpty {
            tip = .quoteSelection
        }
        
        return ContextController.Items(id: AnyHashable("reply"), content: .list(items), tip: tip)
    }
    
    let _ = ApplicationSpecificNotice.incrementReplyQuoteTextSelectionTips(accountManager: selfController.context.sharedContext.accountManager).startStandalone()
    
    return items
}

private func chatReplyOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode, getContextController: @escaping () -> ContextController?, selectionState: Promise<ChatControllerSubject.MessageOptionsInfo.SelectionState>) -> ContextController.Source? {
    guard let peerId = selfController.chatLocation.peerId else {
        return nil
    }
    guard let replySubject = selfController.presentationInterfaceState.interfaceState.replyMessageSubject else {
        return nil
    }
    
    var replyQuote: ChatControllerSubject.MessageOptionsInfo.Quote?
    if let quote = replySubject.quote {
        replyQuote = ChatControllerSubject.MessageOptionsInfo.Quote(messageId: replySubject.messageId, text: quote.text, offset: quote.offset)
    }
    selectionState.set(selfController.context.account.postbox.messagesAtIds([replySubject.messageId])
    |> map { messages -> ChatControllerSubject.MessageOptionsInfo.SelectionState in
        var canQuote = false
        if let message = messages.first, !message.text.isEmpty {
            canQuote = true
        }
        return ChatControllerSubject.MessageOptionsInfo.SelectionState(
            canQuote: canQuote,
            quote: replyQuote
        )
    }
    |> distinctUntilChanged)
    
    guard let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [replySubject.messageId.peerId], ids: [replySubject.messageId], info: .reply(ChatControllerSubject.MessageOptionsInfo.Reply(quote: replyQuote, selectionState: selectionState))), botStart: nil, mode: .standard(.previewing)) as? ChatControllerImpl else {
        return nil
    }
    chatController.canReadHistory.set(false)
    
    let items = generateChatReplyOptionItems(selfController: selfController, chatController: chatController)
    
    chatController.performTextSelectionAction = { [weak selfController] message, canCopy, text, action in
        guard let selfController, let contextController = getContextController() else {
            return
        }
        
        contextController.dismiss()
        
        selfController.controllerInteraction?.performTextSelectionAction(message, canCopy, text, action)
    }
    
    return ContextController.Source(
        id: AnyHashable(OptionsId.reply),
        title: selfController.presentationData.strings.Conversation_MessageOptionsTabReply,
        source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)),
        items: items
    )
}

func presentChatReplyOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode) {
    presentChatInputOptions(selfController: selfController, sourceNode: sourceNode, initialId: .reply)
}

func moveReplyMessageToAnotherChat(selfController: ChatControllerImpl, replySubject: ChatInterfaceState.ReplyMessageSubject) {
    let _ = selfController.presentVoiceMessageDiscardAlert(action: { [weak selfController] in
        guard let selfController else {
            return
        }
        let filter: ChatListNodePeersFilter = [.onlyWriteable, .includeSavedMessages, .excludeDisabled, .doNotSearchMessages]
        var attemptSelectionImpl: ((EnginePeer, ChatListDisabledPeerReason) -> Void)?
        let controller = selfController.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(
            context: selfController.context,
            updatedPresentationData: selfController.updatedPresentationData,
            filter: filter,
            hasFilters: true,
            title: selfController.presentationData.strings.Conversation_MoveReplyToAnotherChatTitle,
            attemptSelection: { peer, _, reason in
                attemptSelectionImpl?(peer, reason)
            },
            multipleSelection: false,
            forwardedMessageIds: [],
            selectForumThreads: true
        ))
        let context = selfController.context
        attemptSelectionImpl = { [weak selfController, weak controller] peer, reason in
            guard let selfController, let controller = controller else {
                return
            }
            let presentationData = context.sharedContext.currentPresentationData.with { $0 }
            switch reason {
            case .generic:
                controller.present(textAlertController(context: context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: presentationData.strings.Forward_ErrorDisabledForChat, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
            case .premiumRequired:
                controller.forEachController { c in
                    if let c = c as? UndoOverlayController {
                        c.dismiss()
                    }
                    return true
                }
                controller.present(UndoOverlayController(presentationData: presentationData, content: .premiumPaywall(title: nil, text: presentationData.strings.Chat_ToastMessagingRestrictedToPremium_Text(peer.compactDisplayTitle).string, customUndoText: presentationData.strings.Chat_ToastMessagingRestrictedToPremium_Action, timeout: nil, linkAction: { _ in
                }), elevatedLayout: false, animateInAsReplacement: true, action: { [weak selfController, weak controller] action in
                    guard let selfController, let controller else {
                        return false
                    }
                    if case .undo = action {
                        let premiumController = PremiumIntroScreen(context: selfController.context, source: .settings)
                        controller.push(premiumController)
                    }
                    return false
                }), in: .current)
            }
        }
        controller.peerSelected = { [weak selfController, weak controller] peer, threadId in
            guard let selfController, let strongController = controller else {
                return
            }
            let peerId = peer.id
            //let accountPeerId = selfController.context.account.peerId
            
            var isPinnedMessages = false
            if case .pinnedMessages = selfController.presentationInterfaceState.subject {
                isPinnedMessages = true
            }
            
            if case .peer(peerId) = selfController.chatLocation, selfController.parentController == nil, !isPinnedMessages {
                selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(replySubject).withoutSelectionState() }).updatedSearch(nil) })
                selfController.updateItemNodesSearchTextHighlightStates()
                selfController.searchResultsController = nil
                strongController.dismiss()
            } else {
                if let navigationController = selfController.navigationController as? NavigationController {
                    for controller in navigationController.viewControllers {
                        if let maybeChat = controller as? ChatControllerImpl {
                            if case .peer(peerId) = maybeChat.chatLocation {
                                var isChatPinnedMessages = false
                                if case .pinnedMessages = maybeChat.presentationInterfaceState.subject {
                                    isChatPinnedMessages = true
                                }
                                if !isChatPinnedMessages {
                                    maybeChat.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(replySubject).withoutSelectionState() }) })
                                    selfController.dismiss()
                                    strongController.dismiss()
                                    return
                                }
                            }
                        }
                    }
                }

                let _ = (ChatInterfaceState.update(engine: selfController.context.engine, peerId: peerId, threadId: threadId, { currentState in
                    return currentState.withUpdatedReplyMessageSubject(replySubject)
                })
                |> deliverOnMainQueue).startStandalone(completed: { [weak selfController] in
                    guard let selfController else {
                        return
                    }
                    let proceed: (ChatController) -> Void = { [weak selfController] chatController in
                        guard let selfController else {
                            return
                        }
                        selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(nil).withoutSelectionState() }) })
                        
                        let navigationController: NavigationController?
                        if let parentController = selfController.parentController {
                            navigationController = (parentController.navigationController as? NavigationController)
                        } else {
                            navigationController = selfController.effectiveNavigationController
                        }
                        
                        if let navigationController = navigationController {
                            var viewControllers = navigationController.viewControllers
                            if threadId != nil {
                                viewControllers.insert(chatController, at: viewControllers.count - 2)
                            } else {
                                viewControllers.insert(chatController, at: viewControllers.count - 1)
                            }
                            navigationController.setViewControllers(viewControllers, animated: false)
                            
                            selfController.controllerNavigationDisposable.set((chatController.ready.get()
                            |> SwiftSignalKit.filter { $0 }
                            |> take(1)
                            |> deliverOnMainQueue).startStrict(next: { [weak navigationController] _ in
                                viewControllers.removeAll(where: { $0 is PeerSelectionController })
                                navigationController?.setViewControllers(viewControllers, animated: true)
                            }))
                        }
                    }
                    if let threadId = threadId {
                        let _ = (selfController.context.sharedContext.chatControllerForForumThread(context: selfController.context, peerId: peerId, threadId: threadId)
                        |> deliverOnMainQueue).startStandalone(next: { chatController in
                            proceed(chatController)
                        })
                    } else {
                        let chatController = ChatControllerImpl(context: selfController.context, chatLocation: .peer(id: peerId))
                        chatController.activateInput(type: .text)
                        proceed(chatController)
                    }
                })
            }
        }
        selfController.chatDisplayNode.dismissInput()
        selfController.effectiveNavigationController?.pushViewController(controller)
    })
}

private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode, getContextController: @escaping () -> ContextController?, replySelectionState: Promise<ChatControllerSubject.MessageOptionsInfo.SelectionState>) -> ContextController.Source? {
    guard let peerId = selfController.chatLocation.peerId else {
        return nil
    }
    
    let initialUrlPreview: ChatPresentationInterfaceState.UrlPreview?
    if selfController.presentationInterfaceState.interfaceState.editMessage != nil {
        initialUrlPreview = selfController.presentationInterfaceState.editingUrlPreview
    } else {
        initialUrlPreview = selfController.presentationInterfaceState.urlPreview
    }
    
    guard let initialUrlPreview else {
        return nil
    }
    
    let linkOptions = combineLatest(queue: .mainQueue(),
        selfController.presentationInterfaceStatePromise.get(),
        replySelectionState.get()
    )
    |> map { state, replySelectionState -> ChatControllerSubject.LinkOptions in
        let urlPreview: ChatPresentationInterfaceState.UrlPreview
        if state.interfaceState.editMessage != nil {
            urlPreview = state.editingUrlPreview ?? initialUrlPreview
        } else {
            urlPreview = state.urlPreview ?? initialUrlPreview
        }
        
        var webpageHasLargeMedia = false
        if case let .Loaded(content) = urlPreview.webPage.content {
            if let isMediaLargeByDefault = content.isMediaLargeByDefault {
                if isMediaLargeByDefault {
                    webpageHasLargeMedia = true
                }
            } else {
                webpageHasLargeMedia = true
            }
        }
        
        let composeInputText: NSAttributedString = state.interfaceState.effectiveInputState.inputText
        
        var replyMessageId: EngineMessage.Id?
        var replyQuote: String?
        
        if state.interfaceState.editMessage == nil {
            replyMessageId = state.interfaceState.replyMessageSubject?.messageId
            replyQuote = replySelectionState.quote?.text
        }
        
        let inputText = chatInputStateStringWithAppliedEntities(composeInputText.string, entities: generateChatInputTextEntities(composeInputText, generateLinks: false))
        
        var largeMedia = false
        if webpageHasLargeMedia {
            if let value = urlPreview.largeMedia {
                largeMedia = value
            } else if case let .Loaded(content) = urlPreview.webPage.content {
                largeMedia = !defaultWebpageImageSizeIsSmall(webpage: content)
            } else {
                largeMedia = true
            }
        } else {
            largeMedia = false
        }
        
        return ChatControllerSubject.LinkOptions(
            messageText: composeInputText.string,
            messageEntities: generateChatInputTextEntities(composeInputText, generateLinks: true),
            hasAlternativeLinks: detectUrls(inputText).count > 1,
            replyMessageId: replyMessageId,
            replyQuote: replyQuote,
            url: urlPreview.url,
            webpage: urlPreview.webPage,
            linkBelowText: urlPreview.positionBelowText,
            largeMedia: largeMedia
        )
    }
    |> distinctUntilChanged
    
    guard let chatController = selfController.context.sharedContext.makeChatController(context: selfController.context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [peerId], ids: selfController.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], info: .link(ChatControllerSubject.MessageOptionsInfo.Link(options: linkOptions))), botStart: nil, mode: .standard(.previewing)) as? ChatControllerImpl else {
        return nil
    }
    chatController.canReadHistory.set(false)
    
    let items = linkOptions
    |> deliverOnMainQueue
    |> map { [weak selfController] linkOptions -> ContextController.Items in
        guard let selfController else {
            return ContextController.Items(id: AnyHashable(linkOptions.url), content: .list([]))
        }
        var items: [ContextMenuItem] = []
        
        do {
            items.append(.action(ContextMenuActionItem(text: linkOptions.linkBelowText ? selfController.presentationData.strings.Conversation_MessageOptionsLinkMoveUp : selfController.presentationData.strings.Conversation_MessageOptionsLinkMoveDown, icon: { theme in
                return nil
            }, iconAnimation: ContextMenuActionItem.IconAnimation(
                name: linkOptions.linkBelowText ? "message_preview_sort_above" : "message_preview_sort_below"
            ), action: { [weak selfController] _, f in
                selfController?.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
                    if state.interfaceState.editMessage != nil {
                        guard var urlPreview = state.editingUrlPreview else {
                            return state
                        }
                        urlPreview.positionBelowText = !urlPreview.positionBelowText
                        return state.updatedEditingUrlPreview(urlPreview)
                    } else {
                        guard var urlPreview = state.urlPreview else {
                            return state
                        }
                        urlPreview.positionBelowText = !urlPreview.positionBelowText
                        return state.updatedUrlPreview(urlPreview)
                    }
                })
            })))
        }
        
        if case let .Loaded(content) = linkOptions.webpage.content, let isMediaLargeByDefault = content.isMediaLargeByDefault, isMediaLargeByDefault {
            let shrinkTitle: String
            let enlargeTitle: String
            if let file = content.file, file.isVideo {
                shrinkTitle = selfController.presentationData.strings.Conversation_MessageOptionsShrinkVideo
                enlargeTitle = selfController.presentationData.strings.Conversation_MessageOptionsEnlargeVideo
            } else {
                shrinkTitle = selfController.presentationData.strings.Conversation_MessageOptionsShrinkImage
                enlargeTitle = selfController.presentationData.strings.Conversation_MessageOptionsEnlargeImage
            }
            
            items.append(.action(ContextMenuActionItem(text: linkOptions.largeMedia ? shrinkTitle : enlargeTitle, icon: { _ in
                return nil
            }, iconAnimation: ContextMenuActionItem.IconAnimation(
                name: !linkOptions.largeMedia ? "message_preview_media_large" : "message_preview_media_small"
            ), action: { [weak selfController] _, f in
                selfController?.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
                    if state.interfaceState.editMessage != nil {
                        guard var urlPreview = state.editingUrlPreview else {
                            return state
                        }
                        if let largeMedia = urlPreview.largeMedia {
                            urlPreview.largeMedia = !largeMedia
                        } else {
                            urlPreview.largeMedia = false
                        }
                        return state.updatedEditingUrlPreview(urlPreview)
                    } else {
                        guard var urlPreview = state.urlPreview else {
                            return state
                        }
                        if let largeMedia = urlPreview.largeMedia {
                            urlPreview.largeMedia = !largeMedia
                        } else {
                            urlPreview.largeMedia = false
                        }
                        return state.updatedUrlPreview(urlPreview)
                    }
                })
            })))
        }
        
        if !items.isEmpty {
            items.append(.separator)
        }
        
        items.append(.action(ContextMenuActionItem(text: selfController.presentationData.strings.Conversation_MessageOptionsApplyChanges, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { _, f in
            f(.default)
        })))
        
        items.append(.action(ContextMenuActionItem(text: selfController.presentationData.strings.Conversation_LinkOptionsCancel, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak selfController, weak chatController] c, f in
            guard let selfController else {
                return
            }
            
            selfController.chatDisplayNode.dismissUrlPreview()
            
            let _ = chatController
            
            f(.default)
        })))
        
        return ContextController.Items(id: AnyHashable(linkOptions.url), content: .list(items))
    }
    
    var webpageCache: [String: TelegramMediaWebpage] = [:]
    chatController.performOpenURL = { [weak selfController] message, url, progress in
        guard let selfController else {
            return
        }
        
        if let (updatedUrlPreviewState, signal) = urlPreviewStateForInputText(NSAttributedString(string: url), context: selfController.context, currentQuery: nil, forPeerId: selfController.chatLocation.peerId), let updatedUrlPreviewState, let detectedUrl = updatedUrlPreviewState.detectedUrls.first {
            if let webpage = webpageCache[detectedUrl] {
                progress?.set(.single(false))
                
                selfController.updateChatPresentationInterfaceState(animated: true, interactive: false, { state in
                    if state.interfaceState.editMessage != nil {
                        if var urlPreview = state.editingUrlPreview {
                            urlPreview.url = detectedUrl
                            urlPreview.webPage = webpage
                            
                            return state.updatedEditingUrlPreview(urlPreview)
                        } else {
                            return state
                        }
                    } else {
                        if var urlPreview = state.urlPreview {
                            urlPreview.url = detectedUrl
                            urlPreview.webPage = webpage
                            
                            return state.updatedUrlPreview(urlPreview)
                        } else {
                            return state
                        }
                    }
                })
            } else {
                progress?.set(.single(true))
                let _ = (signal
                |> afterDisposed {
                    progress?.set(.single(false))
                }
                |> deliverOnMainQueue).start(next: { [weak selfController] result in
                    guard let selfController else {
                        return
                    }
                    
                    selfController.updateChatPresentationInterfaceState(animated: true, interactive: false, { state in
                        if state.interfaceState.editMessage != nil {
                            if let (webpage, webpageUrl) = result(nil), var urlPreview = state.editingUrlPreview {
                                urlPreview.url = webpageUrl
                                urlPreview.webPage = webpage
                                webpageCache[detectedUrl] = webpage
                                
                                return state.updatedEditingUrlPreview(urlPreview)
                            } else {
                                return state
                            }
                        } else {
                            if let (webpage, webpageUrl) = result(nil), var urlPreview = state.urlPreview {
                                urlPreview.url = webpageUrl
                                urlPreview.webPage = webpage
                                webpageCache[detectedUrl] = webpage
                                
                                return state.updatedUrlPreview(urlPreview)
                            } else {
                                return state
                            }
                        }
                    })
                })
            }
        }
    }
    
    return ContextController.Source(
        id: AnyHashable(OptionsId.link),
        title: selfController.presentationData.strings.Conversation_MessageOptionsTabLink,
        source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)),
        items: items
    )
}

func presentChatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASDisplayNode) {
    presentChatInputOptions(selfController: selfController, sourceNode: sourceNode, initialId: .link)
}