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

import SignalServiceKit
import SignalUI

public class CVMediaView: ManualLayoutViewWithLayer {

    private enum MediaError {
        case missing
        case invalid
    }

    // MARK: -

    private let mediaCache: CVMediaCache
    public let attachment: CVAttachment
    private let interaction: TSInteraction
    private let conversationStyle: ConversationStyle
    private let maxMessageWidth: CGFloat
    private let isBorderless: Bool
    private let isLoopingVideo: Bool
    private let thumbnailQuality: AttachmentThumbnailQuality
    private let isBroken: Bool
    private var reusableMediaView: ReusableMediaView?

    // MARK: - Initializers

    public init(
        mediaCache: CVMediaCache,
        attachment: CVAttachment,
        interaction: TSInteraction,
        maxMessageWidth: CGFloat,
        isBorderless: Bool,
        isLoopingVideo: Bool,
        isBroken: Bool,
        thumbnailQuality: AttachmentThumbnailQuality,
        conversationStyle: ConversationStyle
    ) {
        self.mediaCache = mediaCache
        self.attachment = attachment
        self.interaction = interaction
        self.maxMessageWidth = maxMessageWidth
        self.isBorderless = isBorderless
        self.isLoopingVideo = isLoopingVideo
        self.isBroken = isBroken
        self.thumbnailQuality = thumbnailQuality
        self.conversationStyle = conversationStyle

        super.init(name: "CVMediaView")

        backgroundColor = isBorderless ? .clear : Theme.washColor
        clipsToBounds = true

        createContents()
    }

    // MARK: -

    private func createContents() {
        AssertIsOnMainThread()

        switch attachment {
        case .pointer(let pointer, _):
            return configureForUndownloadedMedia(pointer.attachmentPointer)
        case .stream(let attachmentStream):
            let attachmentStream = attachmentStream.attachmentStream
            switch attachmentStream.computeContentType() {
            case .image:
                configureForStillImage(attachmentStream: attachmentStream)
            case .animatedImage:
                configureForAnimatedImage(attachmentStream: attachmentStream)
            case .video where isLoopingVideo:
                configureForLoopingVideo(attachmentStream: attachmentStream)
            case .video:
                configureForVideo(attachmentStream: attachmentStream)
            case .audio, .file, .invalid:
                owsFailDebug("Attachment has unexpected type.")
                configure(forError: .invalid)
            }
        }
    }

    private func configureForUndownloadedMedia(_ pointer: TSResourcePointer) {
        tryToConfigureForBlurHash(pointer: pointer)

        _ = addProgressIfNecessary()
    }

    private func addProgressIfNecessary() -> Bool {

        let direction: CVAttachmentProgressView.Direction
        switch CVAttachmentProgressView.progressType(
            forAttachment: attachment,
            interaction: interaction
        ) {
        case .none:
            return false
        case .uploading(let attachmentStream):
            direction = .upload(attachmentStream: attachmentStream)
        case .pendingDownload:
            // We don't need to add a download indicator for pending
            // attachments; CVComponentBodyMedia will add a download
            // button if any media in the gallery is pending.
            return false
        case .downloading(let attachmentPointer, let transitTierDownloadState):
            backgroundColor = (Theme.isDarkThemeEnabled ? .ows_gray90 : .ows_gray05)

            direction = .download(
                attachmentPointer: attachmentPointer,
                transitTierDownloadState: transitTierDownloadState
            )
        case .unknown:
            owsFailDebug("Unknown progress type.")
            return false
        }

        let progressView = CVAttachmentProgressView(direction: direction,
                                                    isDarkThemeEnabled: conversationStyle.isDarkThemeEnabled,
                                                    mediaCache: mediaCache)
        addSubviewToCenterOnSuperview(progressView, size: progressView.layoutSize)

        return true
    }

    private func configureImageView(_ imageView: UIImageView) {
        // We need to specify a contentMode since the size of the image
        // might not match the aspect ratio of the view.
        imageView.contentMode = .scaleAspectFill
        // Use trilinear filters for better scaling quality at
        // some performance cost.
        imageView.layer.minificationFilter = .trilinear
        imageView.layer.magnificationFilter = .trilinear
    }

    private func applyReusableMediaView(_ reusableMediaView: ReusableMediaView) {
        reusableMediaView.owner = self
        self.reusableMediaView = reusableMediaView
        let mediaView = reusableMediaView.mediaView

        mediaView.removeFromSuperview()
        mediaView.translatesAutoresizingMaskIntoConstraints = false
        addSubviewToFillSuperviewEdges(mediaView)

        if let imageView = mediaView as? UIImageView {
            configureImageView(imageView)
        }
        mediaView.backgroundColor = isBorderless ? .clear : Theme.washColor

        if !addProgressIfNecessary() {
            if reusableMediaView.isVideo {
                addVideoPlayButton()
            }
        }
    }

    private func createNewReusableMediaView(mediaViewAdapter: MediaViewAdapter, isAnimated: Bool) {
        let reusableMediaView = ReusableMediaView(mediaViewAdapter: mediaViewAdapter, mediaCache: mediaCache)
        mediaCache.setMediaView(reusableMediaView, forKey: mediaViewAdapter.cacheKey, isAnimated: isAnimated)
        applyReusableMediaView(reusableMediaView)
    }

    private func tryToConfigureForBlurHash(pointer: TSResourcePointer) {
        guard let blurHash = pointer.resource.resourceBlurHash?.nilIfEmpty else { return }
        // NOTE: in the blurhash case, we use the blurHash itself as the
        // cachekey to avoid conflicts with the actual attachment contents.
        let cacheKey = CVMediaCache.CacheKey.blurHash(blurHash)
        let isAnimated = false
        if let reusableMediaView = mediaCache.getMediaView(cacheKey, isAnimated: isAnimated) {
            applyReusableMediaView(reusableMediaView)
            return
        }

        let mediaViewAdapter = MediaViewAdapterBlurHash(blurHash: blurHash)
        createNewReusableMediaView(mediaViewAdapter: mediaViewAdapter, isAnimated: isAnimated)
    }

    private func configureForLoopingVideo(attachmentStream: TSResourceStream) {
        if let reusableMediaView = mediaCache.getMediaView(
            .attachment(attachmentStream.resourceId),
            isAnimated: true
        ) {
            applyReusableMediaView(reusableMediaView)
        } else {
            createNewReusableMediaView(
                mediaViewAdapter: MediaViewAdapterLoopingVideo(
                    attachmentStream: attachmentStream),
                isAnimated: true)
        }
    }

    private func configureForAnimatedImage(attachmentStream: TSResourceStream) {
        let cacheKey = CVMediaCache.CacheKey.attachment(attachmentStream.resourceId)
        let isAnimated = attachmentStream.computeContentType().isAnimatedImage
        if let reusableMediaView = mediaCache.getMediaView(cacheKey, isAnimated: isAnimated) {
            applyReusableMediaView(reusableMediaView)
            return
        }

        let mediaViewAdapter = MediaViewAdapterAnimated(attachmentStream: attachmentStream)
        createNewReusableMediaView(mediaViewAdapter: mediaViewAdapter, isAnimated: isAnimated)
    }

    private func configureForStillImage(attachmentStream: TSResourceStream) {
        let cacheKey = CVMediaCache.CacheKey.attachment(attachmentStream.resourceId)
        let isAnimated = attachmentStream.computeContentType().isAnimatedImage
        if let reusableMediaView = mediaCache.getMediaView(cacheKey, isAnimated: isAnimated) {
            applyReusableMediaView(reusableMediaView)
            return
        }

        let mediaViewAdapter = MediaViewAdapterStill(attachmentStream: attachmentStream,
                                                     thumbnailQuality: thumbnailQuality)
        createNewReusableMediaView(mediaViewAdapter: mediaViewAdapter, isAnimated: isAnimated)
    }

    private func configureForVideo(attachmentStream: TSResourceStream) {
        let cacheKey = CVMediaCache.CacheKey.attachment(attachmentStream.resourceId)
        let isAnimated = attachmentStream.computeContentType().isAnimatedImage
        if let reusableMediaView = mediaCache.getMediaView(cacheKey, isAnimated: isAnimated) {
            applyReusableMediaView(reusableMediaView)
            return
        }

        let mediaViewAdapter = MediaViewAdapterVideo(attachmentStream: attachmentStream,
                                                     thumbnailQuality: thumbnailQuality)
        createNewReusableMediaView(mediaViewAdapter: mediaViewAdapter, isAnimated: isAnimated)
    }

    private func addVideoPlayButton() {

        let playVideoButtonWidth: CGFloat = 44
        let playVideoIconWidth: CGFloat = 20

        let playVideoButton = UIView.transparentContainer()
        addSubviewToCenterOnSuperview(playVideoButton, size: CGSize(square: playVideoButtonWidth))

        let playVideoCircleView = OWSLayerView.circleView()
        playVideoCircleView.backgroundColor = UIColor.ows_black.withAlphaComponent(0.7)
        playVideoCircleView.isUserInteractionEnabled = false
        playVideoButton.addSubview(playVideoCircleView)
        layoutSubviewToFillSuperviewEdges(playVideoCircleView)

        let playVideoIconView = CVImageView()
        if isBroken {
            playVideoIconView.setTemplateImageName("play-slash-fill", tintColor: UIColor.ows_white)
        } else {
            playVideoIconView.setTemplateImageName("play-fill-32", tintColor: UIColor.ows_white)
        }
        playVideoIconView.isUserInteractionEnabled = false
        addSubviewToCenterOnSuperview(playVideoIconView,
                                      size: CGSize(square: playVideoIconWidth))
    }

    private var hasBlurHash: Bool {
        return BlurHash.isValidBlurHash(attachment.attachment.attachment.resourceBlurHash)
    }

    private func configure(forError error: MediaError) {
        backgroundColor = (Theme.isDarkThemeEnabled ? .ows_gray90 : .ows_gray05)
        let icon: UIImage
        switch error {
        case .invalid:
            guard let asset = UIImage(named: "photo-slash-36") else {
                owsFailDebug("Missing image")
                return
            }
            icon = asset
        case .missing:
            return
        }
        let iconView = CVImageView(image: icon)
        iconView.tintColor = Theme.primaryTextColor.withAlphaComponent(0.6)
        addSubviewToCenterOnSuperview(iconView, size: icon.size)
    }

    public func loadMedia() {
        AssertIsOnMainThread()

        guard let reusableMediaView = reusableMediaView else {
            return
        }
        guard reusableMediaView.owner != nil else {
            Logger.warn("No longer owner of reusableMediaView.")
            return
        }
        guard reusableMediaView.owner == self else {
            owsFailDebug("No longer owner of reusableMediaView.")
            return
        }

        reusableMediaView.load()
    }

    public func unloadMedia() {
        AssertIsOnMainThread()

        guard let reusableMediaView = reusableMediaView else {
            return
        }
        guard reusableMediaView.owner == self else {
            // No longer owner of reusableMediaView.
            return
        }

        reusableMediaView.unload()
    }
}
