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

import SignalServiceKit
import SignalUI

public class CVComponentAudioAttachment: CVComponentBase, CVComponent {

    public var componentKey: CVComponentKey { .audioAttachment }

    private let audioAttachment: AudioAttachment
    private let nextAudioAttachment: AudioAttachment?
    private var attachment: TSResource { audioAttachment.attachment }
    private var attachmentStream: TSResourceStream? { audioAttachment.attachmentStream?.attachmentStream }
    private let footerOverlay: CVComponent?

    init(itemModel: CVItemModel, audioAttachment: AudioAttachment, nextAudioAttachment: AudioAttachment?, footerOverlay: CVComponent?) {
        self.audioAttachment = audioAttachment
        self.nextAudioAttachment = nextAudioAttachment
        self.footerOverlay = footerOverlay

        super.init(itemModel: itemModel)

        databaseStorage.appendDatabaseChangeDelegate(self)
    }

    public func buildComponentView(componentDelegate: CVComponentDelegate) -> CVComponentView {
        CVComponentViewAudioAttachment()
    }

    public func configureForRendering(componentView componentViewParam: CVComponentView,
                                      cellMeasurement: CVCellMeasurement,
                                      componentDelegate: CVComponentDelegate) {
        guard let componentView = componentViewParam as? CVComponentViewAudioAttachment else {
            owsFailDebug("Unexpected componentView.")
            componentViewParam.reset()
            return
        }

        let stackView = componentView.stackView
        let conversationStyle = self.conversationStyle

        if let footerOverlay = self.footerOverlay {
            let footerView: CVComponentView
            if let footerOverlayView = componentView.footerOverlayView {
                footerView = footerOverlayView
            } else {
                let footerOverlayView = CVComponentFooter.CVComponentViewFooter()
                componentView.footerOverlayView = footerOverlayView
                footerView = footerOverlayView
            }
            footerOverlay.configureForRendering(componentView: footerView,
                                                cellMeasurement: cellMeasurement,
                                                componentDelegate: componentDelegate)
            let footerRootView = footerView.rootView

            let footerSize = cellMeasurement.size(key: Self.measurementKey_footerSize) ?? .zero
            stackView.addSubview(footerRootView) { view in
                var footerFrame = view.bounds
                footerFrame.height = min(view.bounds.height, footerSize.height)
                footerFrame.y = view.bounds.height - footerSize.height
                footerRootView.frame = footerFrame
            }
        }

        owsAssertDebug(MimeTypeUtil.isSupportedAudioMimeType(attachment.mimeType))
        let presentation = AudioMessagePresenter(
            isIncoming: isIncoming,
            audioAttachment: audioAttachment,
            threadUniqueId: itemModel.thread.uniqueId,
            playbackRate: AudioPlaybackRate(rawValue: itemModel.itemViewState.audioPlaybackRate))
        let audioMessageView = AudioMessageView(
            presentation: presentation,
            audioMessageViewDelegate: componentDelegate,
            mediaCache: mediaCache
        )
        if let incomingMessage = interaction as? TSIncomingMessage {
            audioMessageView.setViewed(incomingMessage.wasViewed, animated: false)
        } else if let outgoingMessage = interaction as? TSOutgoingMessage {
            audioMessageView.setViewed(!outgoingMessage.viewedRecipientAddresses().isEmpty, animated: false)
        }
        audioMessageView.configureForRendering(
            cellMeasurement: cellMeasurement,
            conversationStyle: conversationStyle
        )
        componentView.audioMessageView = audioMessageView
        stackView.configure(config: stackViewConfig,
                            cellMeasurement: cellMeasurement,
                            measurementKey: Self.measurementKey_stackView,
                            subviews: [ audioMessageView ])

        // Listen for when our audio attachment finishes playing, so we can
        // start playing the next attachment.
        cvAudioPlayer.addListener(self)
    }

    private var stackViewConfig: CVStackViewConfig {
        CVStackViewConfig(axis: .vertical,
                          alignment: .fill,
                          spacing: 0,
                          layoutMargins: .zero)
    }

    private static let measurementKey_stackView = "CVComponentAudioAttachment.measurementKey_stackView"
    private static let measurementKey_footerSize = "CVComponentAudioAttachment.measurementKey_footerSize"

    public func measure(maxWidth: CGFloat, measurementBuilder: CVCellMeasurement.Builder) -> CGSize {
        owsAssertDebug(maxWidth > 0)

        let maxWidth = min(maxWidth, conversationStyle.maxAudioMessageWidth)

        if let footerOverlay = self.footerOverlay {
            let maxFooterWidth = max(0, maxWidth - conversationStyle.textInsets.totalWidth)
            let footerSize = footerOverlay.measure(maxWidth: maxFooterWidth,
                                                   measurementBuilder: measurementBuilder)
            measurementBuilder.setSize(key: Self.measurementKey_footerSize, size: footerSize)
        }

        let presentation = AudioMessagePresenter(
            isIncoming: false,
            audioAttachment: audioAttachment,
            threadUniqueId: itemModel.thread.uniqueId,
            playbackRate: AudioPlaybackRate(rawValue: itemModel.itemViewState.audioPlaybackRate))
        let audioSize = AudioMessageView.measure(
            maxWidth: maxWidth,
            measurementBuilder: measurementBuilder,
            presentation: presentation).ceil
        let audioInfo = audioSize.asManualSubviewInfo
        let stackMeasurement = ManualStackView.measure(config: stackViewConfig,
                                                       measurementBuilder: measurementBuilder,
                                                       measurementKey: Self.measurementKey_stackView,
                                                       subviewInfos: [ audioInfo ],
                                                       maxWidth: maxWidth)
        var measuredSize = stackMeasurement.measuredSize
        measuredSize.width = maxWidth
        return measuredSize
    }

    /// Checks if the message still exists and stops playback if it does not.
    private func checkIfMessageStillExists() {
        let messageWasDeleted = databaseStorage.read { tx in
            TSInteraction.anyFetch(uniqueId: interaction.uniqueId, transaction: tx) == nil
        }

        if messageWasDeleted,
           cvAudioPlayer.audioPlaybackState(forAttachmentId: attachment.resourceId) == .playing {
            cvAudioPlayer.stopAll()
        }
    }

    // MARK: - Events

    public override func handleTap(
        sender: UIGestureRecognizer,
        componentDelegate: CVComponentDelegate,
        componentView: CVComponentView,
        renderItem: CVRenderItem
    ) -> Bool {
        if
            let audioMessageView = (componentView as? CVComponentViewAudioAttachment)?.audioMessageView,
            audioMessageView.handleTap(sender: sender, itemModel: renderItem.itemModel)
        {
            return true
        }

        if audioAttachment.isDownloaded {
            cvAudioPlayer.setPlaybackRate(
                renderItem.itemViewState.audioPlaybackRate,
                forThreadUniqueId: renderItem.itemModel.thread.uniqueId
            )
            cvAudioPlayer.togglePlayState(forAudioAttachment: audioAttachment)
            return true

        } else if audioAttachment.isDownloading, let pointerId = audioAttachment.attachmentPointer?.attachment.resourceId {
            Logger.debug("Cancelling in-progress download because of user action: \(interaction.uniqueId):\(pointerId)")
            self.databaseStorage.write { tx in
                DependenciesBridge.shared.tsResourceDownloadManager.cancelDownload(
                    for: pointerId,
                    tx: tx.asV2Write
                )
            }
            return true

        } else if let message = interaction as? TSMessage {
            Logger.debug("Retrying download for message: \(message.uniqueId)")
            componentDelegate.didTapFailedOrPendingDownloads(message)
            return true

        } else {
            owsFailDebug("Unexpected message type")
            return false
        }
    }

    // MARK: - Scrub Audio With Pan

    public override func findPanHandler(sender: UIPanGestureRecognizer,
                                        componentDelegate: CVComponentDelegate,
                                        componentView: CVComponentView,
                                        renderItem: CVRenderItem,
                                        messageSwipeActionState: CVMessageSwipeActionState) -> CVPanHandler? {
        AssertIsOnMainThread()

        guard let componentView = componentView as? CVComponentViewAudioAttachment else {
            owsFailDebug("Unexpected componentView.")
            return nil
        }
        let itemViewModel = CVItemViewModelImpl(renderItem: renderItem)
        guard componentDelegate.shouldAllowReplyForItem(itemViewModel) else {
            return nil
        }
        guard nil != attachmentStream else {
            return nil
        }
        guard let audioMessageView = componentView.audioMessageView else {
            owsFailDebug("Missing audioMessageView.")
            return nil
        }
        let location = sender.location(in: audioMessageView)
        guard audioMessageView.isPointInScrubbableRegion(location) else {
            return nil
        }

        return CVPanHandler(delegate: componentDelegate,
                            panType: .scrubAudio,
                            renderItem: renderItem)
    }

    public override func startPanGesture(sender: UIPanGestureRecognizer,
                                         panHandler: CVPanHandler,
                                         componentDelegate: CVComponentDelegate,
                                         componentView: CVComponentView,
                                         renderItem: CVRenderItem,
                                         messageSwipeActionState: CVMessageSwipeActionState) {
        AssertIsOnMainThread()
    }

    public override func handlePanGesture(sender: UIPanGestureRecognizer,
                                          panHandler: CVPanHandler,
                                          componentDelegate: CVComponentDelegate,
                                          componentView: CVComponentView,
                                          renderItem: CVRenderItem,
                                          messageSwipeActionState: CVMessageSwipeActionState) {
        AssertIsOnMainThread()

        guard let componentView = componentView as? CVComponentViewAudioAttachment else {
            owsFailDebug("Unexpected componentView.")
            return
        }
        guard let audioMessageView = componentView.audioMessageView else {
            owsFailDebug("Missing audioMessageView.")
            return
        }
        let location = sender.location(in: audioMessageView)
        guard let attachmentStream = attachmentStream else {
            return
        }
        switch sender.state {
        case .changed:
            let progress = audioMessageView.progressForLocation(location)
            audioMessageView.setOverrideProgress(progress, animated: false)
        case .ended:
            // Only update the actual playback position when the user finishes scrubbing,
            // we still call `scrubToLocation` above in order to update the slider.
            audioMessageView.clearOverrideProgress(animated: false)
            let scrubbedTime = audioMessageView.scrubToLocation(location)
            cvAudioPlayer.setPlaybackProgress(progress: scrubbedTime, forAttachmentStream: attachmentStream)
        case .possible, .began, .failed, .cancelled:
            audioMessageView.clearOverrideProgress(animated: false)
        @unknown default:
            owsFailDebug("Invalid state.")
            audioMessageView.clearOverrideProgress(animated: false)
        }
    }

    // MARK: -

    // Used for rendering some portion of an Conversation View item.
    // It could be the entire item or some part thereof.
    public class CVComponentViewAudioAttachment: NSObject, CVComponentView {

        fileprivate let stackView = ManualStackView(name: "CVComponentViewAudioAttachment.stackView")

        fileprivate var audioMessageView: AudioMessageView?

        fileprivate var footerOverlayView: CVComponentView?

        public var isDedicatedCellView = false

        public var rootView: UIView {
            stackView
        }

        public func setIsCellVisible(_ isCellVisible: Bool) {}

        public func reset() {
            stackView.reset()

            audioMessageView?.removeFromSuperview()
            audioMessageView = nil

            footerOverlayView?.reset()
            footerOverlayView = nil
        }
    }
}

// MARK: - CVAudioPlayerListener

extension CVComponentAudioAttachment: CVAudioPlayerListener {
    func audioPlayerStateDidChange(attachmentId: TSResourceId) {}

    func audioPlayerDidFinish(attachmentId: TSResourceId) {
        guard attachmentId == audioAttachment.attachment.resourceId else { return }
        cvAudioPlayer.autoplayNextAudioAttachmentIfNeeded(nextAudioAttachment)
    }

    func audioPlayerDidMarkViewed(attachmentId: TSResourceId) {}
}

// MARK: - DatabaseChangeDelegate

extension CVComponentAudioAttachment: DatabaseChangeDelegate {
    public func databaseChangesDidUpdate(databaseChanges: SignalServiceKit.DatabaseChanges) {
        guard databaseChanges.didUpdate(interaction: self.interaction) else {
            return
        }

        checkIfMessageStillExists()
    }

    public func databaseChangesDidUpdateExternally() {
        checkIfMessageStillExists()
    }

    public func databaseChangesDidReset() {
        checkIfMessageStillExists()
    }
}

// MARK: -

extension CVComponentAudioAttachment: CVAccessibilityComponent {
    public var accessibilityDescription: String {
        if audioAttachment.isVoiceMessage {
            if audioAttachment.durationSeconds > 0 {
                let format = OWSLocalizedString("ACCESSIBILITY_LABEL_VOICE_MEMO_%d", tableName: "PluralAware",
                                               comment: "Accessibility label for a voice memo. Embeds: {{ the duration of the voice message }}.")
                return String.localizedStringWithFormat(format, Int(audioAttachment.durationSeconds))
            } else {
                return OWSLocalizedString("ACCESSIBILITY_LABEL_VOICE_MEMO",
                                         comment: "Accessibility label for a voice memo.")
            }
        } else {
            // TODO: We could include information about the attachment format.
            return OWSLocalizedString("ACCESSIBILITY_LABEL_AUDIO",
                                     comment: "Accessibility label for audio.")
        }
    }
}
