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

import CoreServices
import Photos
import SignalServiceKit
import SignalUI

extension ConversationViewController: ConversationInputToolbarDelegate {

    public func isBlockedConversation() -> Bool {
        threadViewModel.isBlocked
    }

    public func isGroup() -> Bool {
        isGroupConversation
    }

    public func sendButtonPressed() {
        AssertIsOnMainThread()

        guard hasViewWillAppearEverBegun else {
            owsFailDebug("InputToolbar not yet ready.")
            return
        }
        guard let inputToolbar = inputToolbar else {
            owsFailDebug("Missing inputToolbar.")
            return
        }

        inputToolbar.acceptAutocorrectSuggestion()

        guard let messageBody = inputToolbar.messageBodyForSending else {
            return
        }

        tryToSendTextMessage(messageBody, updateKeyboardState: true)
    }

    public func messageWasSent() {
        AssertIsOnMainThread()

        self.lastMessageSentDate = Date()

        loadCoordinator.clearUnreadMessagesIndicator()
        inputToolbar?.quotedReplyDraft = nil

        if self.preferences.soundInForeground,
           let soundId = Sounds.systemSoundIDForSound(.standard(.messageSent), quiet: true) {
            AudioServicesPlaySystemSound(soundId)
        }
        Self.typingIndicatorsImpl.didSendOutgoingMessage(inThread: thread)
    }

    private func tryToSendTextMessage(_ messageBody: MessageBody, updateKeyboardState: Bool) {
        tryToSendTextMessage(
            messageBody,
            updateKeyboardState: updateKeyboardState,
            untrustedThreshold: Date().addingTimeInterval(-OWSIdentityManagerImpl.Constants.defaultUntrustedInterval)
        )
    }

    private func tryToSendTextMessage(_ messageBody: MessageBody, updateKeyboardState: Bool, untrustedThreshold: Date) {
        AssertIsOnMainThread()

        guard hasViewWillAppearEverBegun else {
            owsFailDebug("View not yet ready.")
            return
        }
        guard let inputToolbar = inputToolbar else {
            owsFailDebug("Missing inputToolbar.")
            return
        }

        guard !isBlockedConversation() else {
            showUnblockConversationUI { [weak self] isBlocked in
                if !isBlocked {
                    self?.tryToSendTextMessage(messageBody, updateKeyboardState: false, untrustedThreshold: untrustedThreshold)
                }
            }
            return
        }

        let newUntrustedThreshold = Date()
        let didShowSNAlert = showSafetyNumberConfirmationIfNecessary(
            confirmationText: SafetyNumberStrings.confirmSendButton,
            untrustedThreshold: untrustedThreshold
        ) { [weak self] didConfirmIdentity in
            guard let self = self else { return }
            if didConfirmIdentity {
                self.tryToSendTextMessage(messageBody, updateKeyboardState: false, untrustedThreshold: newUntrustedThreshold)
            }
        }
        if didShowSNAlert {
            return
        }

        guard !messageBody.text.isEmpty else {
            return
        }

        let didAddToProfileWhitelist = ThreadUtil.addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimerWithSneakyTransaction(thread)

        let editValidationError: EditSendValidationError? = Self.databaseStorage.read { transaction in
            if let editTarget = inputToolbar.editTarget {
                return context.editManager.validateCanSendEdit(
                    targetMessageTimestamp: editTarget.timestamp,
                    thread: self.thread,
                    tx: transaction.asV2Read
                )
            }
            return nil
        }

        if let error = editValidationError {
            OWSActionSheets.showActionSheet(message: error.localizedDescription)
            return
        }

        if let editTarget = inputToolbar.editTarget {
            ThreadUtil.enqueueEditMessage(
                body: messageBody,
                thread: self.thread,
                // If we have _any_ quoted reply populated, keep the existing quoted reply.
                // If its cleared, "change" it to nothing (clear it).
                quotedReplyEdit: inputToolbar.quotedReplyDraft == nil ? .change(()) : .keep,
                linkPreviewDraft: inputToolbar.linkPreviewDraft,
                editTarget: editTarget,
                persistenceCompletionHandler: {
                    AssertIsOnMainThread()
                    self.loadCoordinator.enqueueReload()
                }
            )
        } else {
            ThreadUtil.enqueueMessage(
                body: messageBody,
                thread: self.thread,
                quotedReplyDraft: inputToolbar.quotedReplyDraft,
                linkPreviewDraft: inputToolbar.linkPreviewDraft,
                persistenceCompletionHandler: {
                    AssertIsOnMainThread()
                    self.loadCoordinator.enqueueReload()
                }
            )
        }

        messageWasSent()

        // Clearing the text message is a key part of the send animation.
        // It takes 10-15ms, but we do it inline rather than dispatch async
        // since the send can't feel "complete" without it.
        inputToolbar.clearTextMessage(animated: true)

        let thread = self.thread
        Self.databaseStorage.asyncWrite { transaction in
            // Reload a fresh instance of the thread model; our models are not
            // thread-safe, so it wouldn't be safe to update the model in an
            // async write.
            guard let thread = TSThread.anyFetch(uniqueId: thread.uniqueId, transaction: transaction) else {
                owsFailDebug("Missing thread.")
                return
            }
            thread.update(withDraft: nil,
                          replyInfo: nil,
                          editTargetTimestamp: nil,
                          transaction: transaction)
        }

        if didAddToProfileWhitelist {
            ensureBannerState()
        }

        NotificationCenter.default.post(name: ChatListViewController.clearSearch, object: nil)
    }

    public func sendSticker(_ stickerInfo: StickerInfo) {
        AssertIsOnMainThread()

        ImpactHapticFeedback.impactOccurred(style: .light)

        ThreadUtil.enqueueMessage(withInstalledSticker: stickerInfo, thread: thread)
        messageWasSent()
    }

    public func presentManageStickersView() {
        AssertIsOnMainThread()

        let manageStickersView = ManageStickersViewController()
        let navigationController = OWSNavigationController(rootViewController: manageStickersView)
        presentFormSheet(navigationController, animated: true)
    }

    public func updateToolbarHeight() {
        guard hasViewWillAppearEverBegun else {
            owsFailDebug("InputToolbar not yet ready.")
            return
        }
        guard inputToolbar != nil else {
            owsFailDebug("Missing inputToolbar.")
            return
        }

        updateInputAccessoryPlaceholderHeight()
        updateBottomBarPosition()
        updateContentInsets()
    }

    public func voiceMemoGestureDidStart() {
        AssertIsOnMainThread()
        Logger.info("")

        let kIgnoreMessageSendDoubleTapDurationSeconds: TimeInterval = 2.0
        if let lastMessageSentDate = self.lastMessageSentDate,
           abs(lastMessageSentDate.timeIntervalSinceNow) < kIgnoreMessageSendDoubleTapDurationSeconds {
            // If users double-taps the message send button, the second tap can look like a
            // very short voice message gesture.  We want to ignore such gestures.
            cancelRecordingVoiceMessage()
            return
        }

        checkPermissionsAndStartRecordingVoiceMessage()
    }

    public func voiceMemoGestureDidComplete() {
        AssertIsOnMainThread()
        Logger.info("")

        finishRecordingVoiceMessage(sendImmediately: true)
    }

    public func voiceMemoGestureDidLock() {
        AssertIsOnMainThread()
        Logger.info("")

        inputToolbar?.lockVoiceMemoUI()
    }

    public func voiceMemoGestureDidCancel() {
        AssertIsOnMainThread()
        Logger.info("")

        cancelRecordingVoiceMessage()
    }

    public func voiceMemoGestureWasInterrupted() {
        AssertIsOnMainThread()
        Logger.info("")

        finishRecordingVoiceMessage(sendImmediately: false)
    }

    func sendVoiceMemoDraft(_ voiceMemoDraft: VoiceMessageInterruptedDraft) {
        AssertIsOnMainThread()

        sendVoiceMessageDraft(voiceMemoDraft)
    }

    public func saveDraft() {
        AssertIsOnMainThread()

        guard hasViewWillAppearEverBegun else {
            owsFailDebug("InputToolbar not yet ready.")
            return
        }
        guard let inputToolbar = inputToolbar else {
            owsFailDebug("Missing inputToolbar.")
            return
        }

        if !inputToolbar.isHidden {
            let thread = self.thread
            let currentDraft = inputToolbar.messageBodyForSending
            let quotedReply = inputToolbar.quotedReplyDraft
            let editTarget = inputToolbar.editTarget
            Self.databaseStorage.asyncWrite { transaction in
                // Reload a fresh instance of the thread model; our models are not
                // thread-safe, so it wouldn't be safe to update the model in an
                // async write.
                guard let thread = TSThread.anyFetch(uniqueId: thread.uniqueId, transaction: transaction) else {
                    owsFailDebug("Missing thread.")
                    return
                }

                let didChange = Self.draftHasChanged(
                    currentDraft: currentDraft,
                    quotedReply: quotedReply,
                    editTarget: editTarget,
                    thread: thread,
                    transaction: transaction
                )

                // Persist the draft only if its changed. This avoids unnecessary model changes.
                guard didChange else {
                    return
                }

                let replyInfo: ThreadReplyInfoObjC?
                if
                    let quotedReply,
                    let originalMessageTimestamp = quotedReply.originalMessageTimestamp,
                    let aci = quotedReply.originalMessageAuthorAddress.aci
                {
                    replyInfo = ThreadReplyInfoObjC(ThreadReplyInfo(
                        timestamp: originalMessageTimestamp,
                        author: aci
                    ))
                } else {
                    replyInfo = nil
                }
                var editTargetTimestamp: NSNumber?
                if let timestamp = inputToolbar.editTarget?.timestamp {
                    editTargetTimestamp = NSNumber(value: timestamp)
                }
                thread.update(
                    withDraft: currentDraft,
                    replyInfo: replyInfo,
                    editTargetTimestamp: editTargetTimestamp,
                    transaction: transaction
                )
            }
        }
    }

    private static func draftHasChanged(
        currentDraft: MessageBody?,
        quotedReply: DraftQuotedReplyModel?,
        editTarget: TSOutgoingMessage?,
        thread: TSThread,
        transaction: SDSAnyReadTransaction
    ) -> Bool {
        let currentText = currentDraft?.text ?? ""
        let persistedText = thread.messageDraft ?? ""
        if currentText != persistedText {
            return true
        }

        let currentRanges = currentDraft?.ranges.mentions ?? [:]
        let persistedRanges = thread.messageDraftBodyRanges?.mentions ?? [:]
        if currentRanges != persistedRanges {
            return true
        }

        if
            let threadTimestamp = thread.editTargetTimestamp,
            threadTimestamp.uint64Value != editTarget?.timestamp ?? 0
        {
            return true
        }

        let threadReplyInfoStore = DependenciesBridge.shared.threadReplyInfoStore
        let persistedQuotedReply = threadReplyInfoStore.fetch(for: thread.uniqueId, tx: transaction.asV2Read)
        if quotedReply?.originalMessageTimestamp != persistedQuotedReply?.timestamp {
            return true
        }
        if quotedReply?.originalMessageAuthorAddress.aci != persistedQuotedReply?.author {
            return true
        }
        return false
    }

    public func tryToSendAttachments(_ attachments: [SignalAttachment], messageBody: MessageBody?) {
        tryToSendAttachments(
            attachments,
            messageBody: messageBody,
            untrustedThreshold: Date().addingTimeInterval(-OWSIdentityManagerImpl.Constants.defaultUntrustedInterval)
        )
    }

    private func tryToSendAttachments(_ attachments: [SignalAttachment], messageBody: MessageBody?, untrustedThreshold: Date) {
        AssertIsOnMainThread()

        guard hasViewWillAppearEverBegun else {
            owsFailDebug("InputToolbar not yet ready.")
            return
        }
        guard let inputToolbar = inputToolbar else {
            owsFailDebug("Missing inputToolbar.")
            return
        }

        DispatchMainThreadSafe {
            if self.isBlockedConversation() {
                self.showUnblockConversationUI { [weak self] isBlocked in
                    if !isBlocked {
                        self?.tryToSendAttachments(attachments, messageBody: messageBody, untrustedThreshold: untrustedThreshold)
                    }
                }
                return
            }

            let newUntrustedThreshold = Date()
            let didShowSNAlert = self.showSafetyNumberConfirmationIfNecessary(
                confirmationText: SafetyNumberStrings.confirmSendButton,
                untrustedThreshold: untrustedThreshold
            ) { [weak self] didConfirmIdentity in
                if didConfirmIdentity {
                    self?.tryToSendAttachments(attachments, messageBody: messageBody, untrustedThreshold: newUntrustedThreshold)
                }
            }
            if didShowSNAlert {
                return
            }

            for attachment in attachments {
                if attachment.hasError {
                    Logger.warn("Invalid attachment: \(attachment.errorName ?? "Missing data").")
                    self.showErrorAlert(forAttachment: attachment)
                    return
                }
            }

            let didAddToProfileWhitelist = ThreadUtil.addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimerWithSneakyTransaction(self.thread)

            ThreadUtil.enqueueMessage(
                body: messageBody,
                mediaAttachments: attachments,
                thread: self.thread,
                quotedReplyDraft: inputToolbar.quotedReplyDraft,
                persistenceCompletionHandler: {
                    AssertIsOnMainThread()
                    self.loadCoordinator.enqueueReload()
                }
            )

            self.messageWasSent()

            if didAddToProfileWhitelist {
                self.ensureBannerState()
            }

            NotificationCenter.default.post(name: ChatListViewController.clearSearch, object: nil)
        }
    }

    // MARK: - Accessory View

    public func cameraButtonPressed() {
        AssertIsOnMainThread()

        takePictureOrVideo()
    }

    public func photosButtonPressed() {
        AssertIsOnMainThread()

        chooseFromLibrary()
    }

    public func gifButtonPressed() {
        AssertIsOnMainThread()

        showGifPicker()
    }

    public func fileButtonPressed() {
        AssertIsOnMainThread()

        showDocumentPicker()
    }

    public func contactButtonPressed() {
        AssertIsOnMainThread()

        chooseContactForSending()
    }

    public func locationButtonPressed() {
        AssertIsOnMainThread()

        let locationPicker = LocationPicker()
        locationPicker.delegate = self
        let navigationController = OWSNavigationController(rootViewController: locationPicker)
        dismissKeyBoard()
        presentFormSheet(navigationController, animated: true)
    }

    public func paymentButtonPressed() {
        AssertIsOnMainThread()

        guard let contactThread = thread as? TSContactThread else {
            owsFailDebug("Not a contact thread.")
            return
        }

        dismissKeyBoard()

        if payments.isKillSwitchActive {
            OWSActionSheets.showErrorAlert(message: OWSLocalizedString("SETTINGS_PAYMENTS_CANNOT_SEND_PAYMENTS_KILL_SWITCH",
                                                                      comment: "Error message indicating that payments cannot be sent because the feature is not currently available."))
            return
        }

        if paymentsHelper.isPaymentsVersionOutdated {
            OWSActionSheets.showPaymentsOutdatedClientSheet(title: .cantSendPayment)
            return
        }

        SendPaymentViewController.presentFromConversationView(
            self,
            delegate: self,
            recipientAddress: contactThread.contactAddress,
            initialPaymentAmount: nil,
            isOutgoingTransfer: false
        )
    }

    public func didSelectRecentPhoto(asset: PHAsset, attachment: SignalAttachment) {
        AssertIsOnMainThread()

        dismissKeyBoard()

        let pickerModal = SendMediaNavigationController.showingApprovalWithPickedLibraryMedia(asset: asset,
                                                                                              attachment: attachment,
                                                                                              delegate: self,
                                                                                              dataSource: self)
        presentFullScreen(pickerModal, animated: true)
    }
}

// MARK: -

public extension ConversationViewController {

    func showErrorAlert(forAttachment attachment: SignalAttachment?) {
        AssertIsOnMainThread()
        owsAssertDebug(attachment == nil || attachment?.hasError == true)

        let errorMessage = (attachment?.localizedErrorDescription
                                ?? SignalAttachment.missingDataErrorMessage)

        Logger.error("\(errorMessage)")

        OWSActionSheets.showActionSheet(title: OWSLocalizedString("ATTACHMENT_ERROR_ALERT_TITLE",
                                                                 comment: "The title of the 'attachment error' alert."),
                                        message: errorMessage)
    }

    func showApprovalDialog(forAttachment attachment: SignalAttachment?) {
        AssertIsOnMainThread()

        guard let attachment = attachment else {
            owsFailDebug("attachment was unexpectedly nil")
            showErrorAlert(forAttachment: attachment)
            return
        }
        showApprovalDialog(forAttachments: [ attachment ])
    }

    func showApprovalDialog(forAttachments attachments: [SignalAttachment]) {
        AssertIsOnMainThread()

        guard hasViewWillAppearEverBegun else {
            owsFailDebug("InputToolbar not yet ready.")
            return
        }
        guard let inputToolbar = inputToolbar else {
            owsFailDebug("Missing inputToolbar.")
            return
        }

        let modal = AttachmentApprovalViewController.wrappedInNavController(
            attachments: attachments,
            initialMessageBody: inputToolbar.messageBodyForSending,
            approvalDelegate: self,
            approvalDataSource: self,
            stickerSheetDelegate: self
        )
        presentFullScreen(modal, animated: true)
    }
}

// MARK: -

fileprivate extension ConversationViewController {

    // MARK: - Attachment Picking: Contacts

    func chooseContactForSending() {
        AssertIsOnMainThread()

        dismissKeyBoard()
        contactsViewHelper.checkReadAuthorization(
            purpose: .share,
            performWhenAllowed: {
                let contactsPicker = ContactPickerViewController(allowsMultipleSelection: false, subtitleCellType: .none)
                contactsPicker.delegate = self
                contactsPicker.title = OWSLocalizedString(
                    "CONTACT_PICKER_TITLE",
                    comment: "navbar title for contact picker when sharing a contact"
                )
                self.presentFormSheet(OWSNavigationController(rootViewController: contactsPicker), animated: true)
            },
            presentErrorFrom: self
        )
    }

    // MARK: - Attachment Picking: Documents

    func showDocumentPicker() {
        AssertIsOnMainThread()

        // UIDocumentPickerViewController with asCopy true copies to a temp file within our container.
        // It uses more memory than "open" but lets us avoid working with security scoped URLs.
        let pickerController = UIDocumentPickerViewController(forOpeningContentTypes: [.item],
                                                              asCopy: true)
        pickerController.delegate = self

        dismissKeyBoard()
        presentFormSheet(pickerController, animated: true)
    }

    // MARK: - Media Library

    func takePictureOrVideo() {
        AssertIsOnMainThread()

        ows_askForCameraPermissions { [weak self] cameraGranted in
            guard let self = self else { return }
            guard cameraGranted else {
                Logger.warn("camera permission denied.")
                return
            }
            self.ows_askForMicrophonePermissions { [weak self] micGranted in
                guard let self = self else { return }
                if !micGranted {
                    Logger.warn("proceeding, though mic permission denied.")
                    // We can still continue without mic permissions, but any captured video will
                    // be silent.
                }

                let pickerModal = SendMediaNavigationController.showingCameraFirst()
                pickerModal.sendMediaNavDelegate = self
                pickerModal.sendMediaNavDataSource = self
                pickerModal.modalPresentationStyle = .overFullScreen
                // Defer hiding status bar until modal is fully onscreen
                // to prevent unwanted shifting upwards of the entire presenter VC's view.
                let pickerHidesStatusBar = (pickerModal.topViewController?.prefersStatusBarHidden ?? false)
                if !pickerHidesStatusBar {
                    pickerModal.modalPresentationCapturesStatusBarAppearance = true
                }
                self.dismissKeyBoard()
                self.present(pickerModal, animated: true) {
                    if pickerHidesStatusBar {
                        pickerModal.modalPresentationCapturesStatusBarAppearance = true
                        pickerModal.setNeedsStatusBarAppearanceUpdate()
                    }
                }
            }
        }
    }

    func chooseFromLibrary() {
        AssertIsOnMainThread()

        ows_askForMediaLibraryPermissions { [weak self] granted in
            guard let self = self else { return }
            guard granted else {
                Logger.warn("Media Library permission denied.")
                return
            }

            let pickerModal = SendMediaNavigationController.showingMediaLibraryFirst()
            pickerModal.sendMediaNavDelegate = self
            pickerModal.sendMediaNavDataSource = self

            self.dismissKeyBoard()
            self.presentFullScreen(pickerModal, animated: true)
        }
    }
}

// MARK: - Attachment Picking: GIFs

public extension ConversationViewController {
    func showGifPicker() {
        let gifModal = GifPickerNavigationViewController(initialMessageBody: inputToolbar?.messageBodyForSending)
        gifModal.approvalDelegate = self
        gifModal.approvalDataSource = self
        dismissKeyBoard()
        present(gifModal, animated: true)
    }
}

// MARK: -

extension ConversationViewController: LocationPickerDelegate {

    public func didPickLocation(_ locationPicker: LocationPicker, location: Location) {
        AssertIsOnMainThread()

        firstly(on: DispatchQueue.global()) { () -> Promise<SignalAttachment> in
            location.prepareAttachment()
        }.done(on: DispatchQueue.main) { [weak self] attachment in
            // TODO: Can we move this off the main thread?
            AssertIsOnMainThread()

            guard let self = self else { return }

            let didAddToProfileWhitelist = ThreadUtil.addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimerWithSneakyTransaction(self.thread)

            ThreadUtil.enqueueMessage(body: MessageBody(text: location.messageText,
                                                        ranges: .empty),
                                      mediaAttachments: [ attachment ],
                                      thread: self.thread,
                                      persistenceCompletionHandler: {
                                            AssertIsOnMainThread()
                                            self.loadCoordinator.enqueueReload()
                                      })

            self.messageWasSent()

            if didAddToProfileWhitelist {
                self.ensureBannerState()
            }

            NotificationCenter.default.post(name: ChatListViewController.clearSearch, object: nil)
        }.catch(on: DispatchQueue.global()) { error in
            owsFailDebug("Error: \(error).")
        }
    }
}

// MARK: -

extension ConversationViewController: UIDocumentPickerDelegate {

    public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) {
        Logger.debug("Picked document at url: \(url)")

        let typeIdentifier: String = {
            do {
                let resourceValues = try url.resourceValues(forKeys: Set([
                    .typeIdentifierKey
                ]))
                guard let typeIdentifier = resourceValues.typeIdentifier else {
                    owsFailDebug("Missing typeIdentifier.")
                    return kUTTypeData as String
                }
                return typeIdentifier
            } catch {
                owsFailDebug("Error: \(error)")
                return kUTTypeData as String
            }
        }()
        let isDirectory: Bool = {
            do {
                let resourceValues = try url.resourceValues(forKeys: Set([
                    .isDirectoryKey
                ]))
                guard let isDirectory = resourceValues.isDirectory else {
                    owsFailDebug("Missing isDirectory.")
                    return false
                }
                return isDirectory
            } catch {
                owsFailDebug("Error: \(error)")
                return false
            }
        }()

        if isDirectory {
            Logger.info("User picked directory.")

            DispatchQueue.main.async {
                OWSActionSheets.showActionSheet(title: OWSLocalizedString("ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_TITLE",
                                                                         comment: "Alert title when picking a document fails because user picked a directory/bundle"),
                                                message: OWSLocalizedString("ATTACHMENT_PICKER_DOCUMENTS_PICKED_DIRECTORY_FAILED_ALERT_BODY",
                                                                           comment: "Alert body when picking a document fails because user picked a directory/bundle"))
            }
            return
        }

        let filename: String = {
            if let filename = url.lastPathComponent.strippedOrNil {
                return filename
            }
            owsFailDebug("Unable to determine filename")
            return OWSLocalizedString("ATTACHMENT_DEFAULT_FILENAME",
                                     comment: "Generic filename for an attachment with no known name")
        }()

        func buildDataSource() -> DataSource? {
            do {
                return try DataSourcePath.dataSource(with: url,
                                                     shouldDeleteOnDeallocation: false)
            } catch {
                owsFailDebug("Error: \(error).")
                return nil
            }
        }
        guard let dataSource = buildDataSource() else {
            DispatchQueue.main.async {
                OWSActionSheets.showActionSheet(title: OWSLocalizedString("ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE",
                                                                         comment: "Alert title when picking a document fails for an unknown reason"))
            }
            return
        }
        dataSource.sourceFilename = filename

        // Although we want to be able to send higher quality attachments through the document picker
        // it's more important that we ensure the sent format is one all clients can accept (e.g. *not* quicktime .mov)
        if SignalAttachment.isVideoThatNeedsCompression(dataSource: dataSource,
                                                        dataUTI: typeIdentifier) {
            self.showApprovalDialogAfterProcessingVideoURL(url, filename: filename)
            return
        }

        let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: typeIdentifier)
        showApprovalDialog(forAttachment: attachment)
    }

    private func showApprovalDialogAfterProcessingVideoURL(_ movieURL: URL, filename: String?) {
        AssertIsOnMainThread()

        ModalActivityIndicatorViewController.present(fromViewController: self,
                                                     canCancel: true) { modalActivityIndicator in
            let dataSource: DataSource
            do {
                dataSource = try DataSourcePath.dataSource(with: movieURL, shouldDeleteOnDeallocation: false)
            } catch {
                owsFailDebug("Error: \(error).")

                DispatchQueue.main.async {
                    self.showErrorAlert(forAttachment: nil)
                }
                return
            }

            dataSource.sourceFilename = filename
            let promise = Promise.wrapAsync({
                return try await SignalAttachment.compressVideoAsMp4(dataSource: dataSource,
                                                                     dataUTI: kUTTypeMPEG4 as String)
            })
            promise.done(on: DispatchQueue.main) { (attachment: SignalAttachment) in
                if modalActivityIndicator.wasCancelled {
                    return
                }
                modalActivityIndicator.dismiss {
                    if attachment.hasError {
                        owsFailDebug("Invalid attachment: \(attachment.errorName ?? "Unknown error").")
                        self.showErrorAlert(forAttachment: attachment)
                    } else {
                        self.showApprovalDialog(forAttachment: attachment)
                    }
                }
            }.catch(on: DispatchQueue.main) { error in
                owsFailDebug("Error: \(error).")

                modalActivityIndicator.dismiss {
                    owsFailDebug("Invalid attachment.")
                    self.showErrorAlert(forAttachment: nil)
                }
            }
        }
    }
}

// MARK: -

extension ConversationViewController: SendMediaNavDelegate {

    func sendMediaNavDidCancel(_ sendMediaNavigationController: SendMediaNavigationController) {
        // Restore status bar visibility (if current VC hides it) so that
        // there's no visible UI updates in the presenter.
        if sendMediaNavigationController.topViewController?.prefersStatusBarHidden ?? false {
            sendMediaNavigationController.modalPresentationCapturesStatusBarAppearance = false
            sendMediaNavigationController.setNeedsStatusBarAppearanceUpdate()
        }
        self.dismiss(animated: true, completion: nil)
    }

    func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController,
                      didApproveAttachments attachments: [SignalAttachment],
                      messageBody: MessageBody?) {
        tryToSendAttachments(attachments, messageBody: messageBody)
        inputToolbar?.clearTextMessage(animated: false)
        // we want to already be at the bottom when the user returns, rather than have to watch
        // the new message scroll into view.
        scrollToBottomOfConversation(animated: true)
        self.dismiss(animated: true, completion: nil)
    }

    func sendMediaNav(_ sendMediaNavifationController: SendMediaNavigationController,
                      didFinishWithTextAttachment textAttachment: UnsentTextAttachment) {
        owsFailDebug("Can not post text stories to chat.")
    }

    func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController,
                      didChangeMessageBody newMessageBody: MessageBody?) {
        guard hasViewWillAppearEverBegun else {
            owsFailDebug("InputToolbar not yet ready.")
            return
        }
        guard let inputToolbar = inputToolbar else {
            owsFailDebug("Missing inputToolbar.")
            return
        }

        inputToolbar.setMessageBody(newMessageBody, animated: false)
    }

    func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didChangeViewOnceState isViewOnce: Bool) {
        // We can ignore this event.
    }
}

// MARK: -

extension ConversationViewController: SendMediaNavDataSource {

    func sendMediaNavInitialMessageBody(_ sendMediaNavigationController: SendMediaNavigationController) -> MessageBody? {
        inputToolbar?.messageBodyForSending
    }

    var sendMediaNavTextInputContextIdentifier: String? { textInputContextIdentifier }

    var sendMediaNavRecipientNames: [String] {
        let displayName = databaseStorage.read { tx in contactsManager.displayName(for: thread, transaction: tx) }
        return [displayName]
    }

    func sendMediaNavMentionableAddresses(tx: DBReadTransaction) -> [SignalServiceAddress] {
        supportsMentions ? thread.recipientAddresses(with: SDSDB.shimOnlyBridge(tx)) : []
    }

    func sendMediaNavMentionCacheInvalidationKey() -> String {
        return thread.uniqueId
    }
}

// MARK: - StickerPickerSheetDelegate

extension ConversationViewController: StickerPickerSheetDelegate {
    public func makeManageStickersViewController() -> UIViewController {
        let manageStickersView = ManageStickersViewController()
        let navigationController = OWSNavigationController(rootViewController: manageStickersView)
        return navigationController
    }
}
