//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import ContactsUI
import SignalServiceKit
import SignalUI

public extension ConversationViewController {

    func updateV2GroupIfNecessary() {
        AssertIsOnMainThread()

        guard let groupThread = thread as? TSGroupThread,
              thread.isGroupV2Thread else {
            return
        }
        // Try to update the v2 group to latest from the service.
        // This will help keep us in sync if we've missed any group updates, etc.
        groupV2Updates.tryToRefreshV2GroupUpToCurrentRevisionAfterMessageProcessingWithThrottling(groupThread)
    }

    func showUnblockConversationUI(completion: BlockListUIUtils.Completion?) {
        self.userHasScrolled = false

        // To avoid "noisy" animations (hiding the keyboard before showing
        // the action sheet, re-showing it after), hide the keyboard before
        // showing the "unblock" action sheet.
        //
        // Unblocking is a rare interaction, so it's okay to leave the keyboard
        // hidden.
        dismissKeyBoard()

        BlockListUIUtils.showUnblockThreadActionSheet(thread, from: self, completion: completion)
    }

    // MARK: - Identity

    /**
     * Shows confirmation dialog if at least one of the recipient id's is not confirmed.
     *
     * returns YES if an alert was shown
     *          NO if there were no unconfirmed identities
     */
    func showSafetyNumberConfirmationIfNecessary(
        confirmationText: String,
        untrustedThreshold: Date?,
        completion: @escaping (Bool) -> Void
    ) -> Bool {
        SafetyNumberConfirmationSheet.presentIfNecessary(
            addresses: thread.recipientAddressesWithSneakyTransaction,
            confirmationText: confirmationText,
            untrustedThreshold: untrustedThreshold,
            completion: completion
        )
    }

    // MARK: - Verification

    func noLongerVerifiedIdentityKeys(tx: SDSAnyReadTransaction) -> [SignalServiceAddress: Data] {
        if let groupThread = thread as? TSGroupThread {
            return OWSRecipientIdentity.noLongerVerifiedIdentityKeys(in: groupThread.uniqueId, tx: tx)
        }
        let identityManager = DependenciesBridge.shared.identityManager
        return thread.recipientAddresses(with: tx).reduce(into: [:]) { result, address in
            guard let recipientIdentity = identityManager.recipientIdentity(for: address, tx: tx.asV2Read) else {
                return
            }
            guard recipientIdentity.verificationState == .noLongerVerified else {
                return
            }
            result[address] = recipientIdentity.identityKey
        }
    }

    func resetVerificationStateToDefault(noLongerVerifiedIdentityKeys: [SignalServiceAddress: Data]) {
        AssertIsOnMainThread()

        databaseStorage.write { transaction in
            let identityManager = DependenciesBridge.shared.identityManager
            for (address, identityKey) in noLongerVerifiedIdentityKeys {
                owsAssertDebug(address.isValid)
                _ = identityManager.setVerificationState(
                    .implicit(isAcknowledged: true),
                    of: identityKey,
                    for: address,
                    isUserInitiatedChange: true,
                    tx: transaction.asV2Write
                )
            }
        }
    }

    func showNoLongerVerifiedUI(noLongerVerifiedIdentityKeys: [SignalServiceAddress: Data]) {
        AssertIsOnMainThread()

        switch noLongerVerifiedIdentityKeys.count {
        case 0:
             break

        case 1:
            showFingerprint(address: noLongerVerifiedIdentityKeys.first!.key)

        default:
            showConversationSettingsAndShowVerification()
        }
    }

    // MARK: - Toast

    func presentToastCVC(_ toastText: String) {
        let toastController = ToastController(text: toastText)
        let kToastInset: CGFloat = 10
        let bottomInset = kToastInset + collectionView.contentInset.bottom + view.layoutMargins.bottom
        toastController.presentToastView(from: .bottom, of: self.view, inset: bottomInset)
    }

    func presentMissingQuotedReplyToast() {
        Logger.info("")

        let toastText = OWSLocalizedString("QUOTED_REPLY_ORIGINAL_MESSAGE_DELETED",
                                          comment: "Toast alert text shown when tapping on a quoted message which we cannot scroll to because the local copy of the message was since deleted.")
        presentToastCVC(toastText)
    }

    func presentRemotelySourcedQuotedReplyToast() {
        Logger.info("")

        let toastText = OWSLocalizedString("QUOTED_REPLY_ORIGINAL_MESSAGE_REMOTELY_SOURCED",
                                          comment: "Toast alert text shown when tapping on a quoted message which we cannot scroll to because the local copy of the message didn't exist when the quote was received.")
        presentToastCVC(toastText)
    }

    func presentViewOnceAlreadyViewedToast() {
        Logger.info("")

        let toastText = OWSLocalizedString("VIEW_ONCE_ALREADY_VIEWED_TOAST",
                                          comment: "Toast alert text shown when tapping on a view-once message that has already been viewed.")
        presentToastCVC(toastText)
    }

    func presentViewOnceOutgoingToast() {
        Logger.info("")

        let toastText = OWSLocalizedString("VIEW_ONCE_OUTGOING_TOAST",
                                          comment: "Toast alert text shown when tapping on a view-once message that you have sent.")
        presentToastCVC(toastText)
    }

    // MARK: - Conversation Settings

    func showConversationSettings() {
        showConversationSettings(mode: .default)
    }

    func showConversationSettingsAndShowAllMedia() {
        showConversationSettings(mode: .showAllMedia)
    }

    func showConversationSettingsAndShowVerification() {
        showConversationSettings(mode: .showVerification)
    }

    func showConversationSettingsAndShowMemberRequests() {
        showConversationSettings(mode: .showMemberRequests)
    }

    func showConversationSettings(mode: ConversationSettingsPresentationMode) {
        guard let viewControllersUpToSelf = self.viewControllersUpToSelf else {
            return
        }
        var viewControllers = viewControllersUpToSelf

        let settingsView = ConversationSettingsViewController(
            threadViewModel: threadViewModel,
            isSystemContact: conversationViewModel.isSystemContact,
            spoilerState: viewState.spoilerState
        )
        settingsView.conversationSettingsViewDelegate = self
        viewControllers.append(settingsView)

        switch mode {
        case .default:
            break
        case .showVerification:
            settingsView.showVerificationOnAppear = true
        case .showMemberRequests:
            if let view = settingsView.buildMemberRequestsAndInvitesView() {
                viewControllers.append(view)
            }
        case .showAllMedia:
            viewControllers.append(AllMediaViewController(
                thread: thread,
                spoilerState: viewState.spoilerState,
                name: title
            ))
        }

        navigationController?.setViewControllers(viewControllers, animated: true)
    }

    private var viewControllersUpToSelf: [UIViewController]? {
        AssertIsOnMainThread()

        guard let navigationController = navigationController else {
            owsFailDebug("Missing navigationController.")
            return nil
        }

        if navigationController.topViewController == self {
            return navigationController.viewControllers
        }

        let viewControllers = navigationController.viewControllers
        guard let index = viewControllers.firstIndex(of: self) else {
            owsFailDebug("Unexpectedly missing from view hierarchy")
            return viewControllers
        }

        return Array(viewControllers.prefix(upTo: index + 1))
    }

    // MARK: - Member Action Sheet

    func showMemberActionSheet(forAddress address: SignalServiceAddress, withHapticFeedback: Bool) {
        AssertIsOnMainThread()

        if withHapticFeedback {
            ImpactHapticFeedback.impactOccurred(style: .light)
        }

        var groupViewHelper: GroupViewHelper?
        if threadViewModel.isGroupThread {
            groupViewHelper = GroupViewHelper(threadViewModel: threadViewModel)
            groupViewHelper!.delegate = self
        }

        ProfileSheetSheetCoordinator(
            address: address,
            groupViewHelper: groupViewHelper,
            spoilerState: spoilerState
        )
        .presentAppropriateSheet(from: self)
    }
}

// MARK: -

extension ConversationViewController: ConversationSettingsViewDelegate {

    public func conversationSettingsDidRequestConversationSearch() {
        AssertIsOnMainThread()

        self.uiMode = .search

        self.popAllConversationSettingsViews {
            // This delay is unfortunate, but without it, self.searchController.uiSearchController.searchBar
            // isn't yet ready to become first responder. Presumably we're still mid transition.
            // A hardcorded constant like this isn't great because it's either too slow, making our users
            // wait, or too fast, and fails to wait long enough to be ready to become first responder.
            // Luckily in this case the stakes aren't catastrophic. In the case that we're too aggressive
            // the user will just have to manually tap into the search field before typing.

            // Leaving this assert in as proof that we're not ready to become first responder yet.
            // If this assert fails, *great* maybe we can get rid of this delay.
            owsAssertDebug(!self.searchController.uiSearchController.searchBar.canBecomeFirstResponder)

            // We wait N seconds for it to become ready.
            let initialDelay: TimeInterval = 0.4
            DispatchQueue.main.asyncAfter(deadline: .now() + initialDelay) { [weak self] in
                self?.tryToBecomeFirstResponderForSearch(cumulativeDelay: initialDelay)
            }
        }
    }

    private func popAllConversationSettingsViews(completion: (() -> Void)?) {
        AssertIsOnMainThread()

        guard let presentedViewController = presentedViewController else {
            navigationController?.popToViewController(self, animated: true, completion: completion)
            return
        }
        presentedViewController.dismiss(animated: true) {
            self.navigationController?.popToViewController(self, animated: true, completion: completion)
        }
    }

    // MARK: - Conversation Search

    private func tryToBecomeFirstResponderForSearch(cumulativeDelay: TimeInterval) {
        // If this took more than N seconds, assume we're not going
        // to be able to present search and bail.
        if cumulativeDelay >= 1.5 {
            owsFailDebug("Giving up presenting search after excessive retry attempts.")
            self.uiMode = .normal
            return
        }

        // Sometimes it takes longer, so we'll keep retrying..
        if !searchController.uiSearchController.searchBar.canBecomeFirstResponder {
            let additionalDelay: TimeInterval = 0.05
            DispatchQueue.main.asyncAfter(deadline: .now() + additionalDelay) { [weak self] in
                self?.tryToBecomeFirstResponderForSearch(cumulativeDelay: cumulativeDelay + additionalDelay)
            }
            return
        }

        searchController.uiSearchController.searchBar.becomeFirstResponder()
    }
}

// MARK: - Preview / 3D Touch / UIContextMenu Methods

public extension ConversationViewController {
    var isInPreviewPlatter: Bool {
        get { viewState.isInPreviewPlatter }
        set {
            guard viewState.isInPreviewPlatter != newValue else {
                return
            }
            viewState.isInPreviewPlatter = newValue
            if hasViewWillAppearEverBegun {
                ensureBottomViewType()
            }
            configureScrollDownButtons()
        }
    }

    @objc
    func previewSetup() {
        isInPreviewPlatter = true
        actionOnOpen = .none
    }
}

// MARK: - Timers

extension ConversationViewController {
    public func startReadTimer(caller: String = #function) {
        AssertIsOnMainThread()

        markAsReadLogger.info("Starting read timer for \(caller).")

        readTimer?.invalidate()
        let readTimer = Timer(timeInterval: 0.1, repeats: true) { [weak self] timer in
            guard let self else {
                timer.invalidate()
                return
            }
            self.readTimerDidFire()
        }
        self.readTimer = readTimer
        RunLoop.main.add(readTimer, forMode: .common)
    }

    private func readTimerDidFire() {
        AssertIsOnMainThread()

        if layout.isPerformBatchUpdatesOrReloadDataBeingApplied {
            return
        }
        markVisibleMessagesAsRead()
    }

    public func cancelReadTimer(caller: String = #function) {
        AssertIsOnMainThread()

        markAsReadLogger.info("Canceling read timer for \(caller).")

        readTimer?.invalidate()
        self.readTimer = nil
    }

    private var readTimer: Timer? {
        get { viewState.readTimer }
        set { viewState.readTimer = newValue }
    }

    public var reloadTimer: Timer? {
        get { viewState.reloadTimer }
        set { viewState.reloadTimer = newValue }
    }

    func startReloadTimer() {
        AssertIsOnMainThread()
        let reloadTimer = Timer(timeInterval: 1, repeats: true) { [weak self] timer in
            guard let self else {
                timer.invalidate()
                return
            }
            self.reloadTimerDidFire()
        }
        self.reloadTimer = reloadTimer
        RunLoop.main.add(reloadTimer, forMode: .common)
    }

    private func reloadTimerDidFire() {
        AssertIsOnMainThread()

        if isUserScrolling || !isViewCompletelyAppeared || !isViewVisible
            || !CurrentAppContext().isAppForegroundAndActive() || !viewHasEverAppeared {
            return
        }

        let timeSinceLastReload = abs(self.lastReloadDate.timeIntervalSinceNow)
        let kReloadFrequency: TimeInterval = 60
        if timeSinceLastReload < kReloadFrequency {
            return
        }

        // Auto-load more if necessary...
        if !autoLoadMoreIfNecessary() {
            // ...Otherwise, reload everything.
            //
            // TODO: We could make this cheaper by using enqueueReload()
            // if we moved volatile profile / footer state to the view state.
            loadCoordinator.enqueueReload()
        }
    }

    var lastSortIdMarkedRead: UInt64 {
        get { viewState.lastSortIdMarkedRead }
        set { viewState.lastSortIdMarkedRead = newValue }
    }

    var isMarkingAsRead: Bool {
        get { viewState.isMarkingAsRead }
        set { viewState.isMarkingAsRead = newValue }
    }

    private func setLastSortIdMarkedRead(lastSortIdMarkedRead: UInt64) {
        AssertIsOnMainThread()
        owsAssertDebug(self.isMarkingAsRead)

        self.lastSortIdMarkedRead = lastSortIdMarkedRead
    }

    public func markVisibleMessagesAsRead(caller: String = #function) {
        AssertIsOnMainThread()

        if nil != self.presentedViewController {
            return
        }
        if WindowManager.shared.shouldShowCallView {
            return
        }
        if navigationController?.topViewController != self {
            return
        }

        // Always clear the thread unread flag
        clearThreadUnreadFlagIfNecessary()

        let lastVisibleSortId = self.lastVisibleSortId
        let isShowingUnreadMessage = lastVisibleSortId > self.lastSortIdMarkedRead
        if !self.isMarkingAsRead && isShowingUnreadMessage {
            markAsReadLogger.info("Marking as read for \(caller).")

            self.isMarkingAsRead = true

            Self.receiptManager.markAsReadLocally(
                beforeSortId: lastVisibleSortId,
                thread: self.thread,
                hasPendingMessageRequest: self.threadViewModel.hasPendingMessageRequest
            ) {
                AssertIsOnMainThread()
                self.setLastSortIdMarkedRead(lastSortIdMarkedRead: lastVisibleSortId)
                self.isMarkingAsRead = false

                // If -markVisibleMessagesAsRead wasn't invoked on a
                // timer, we'd want to double check that the current
                // -lastVisibleSortId hasn't incremented since we
                // started the read receipt request. But we have a
                // timer, so if it has changed, this method will just
                // be reinvoked in < 100ms.
            }
        }
    }
}
