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

import SignalServiceKit
import SignalUI

class CVComponentBodyMedia: CVComponentBase, CVComponent {

    var componentKey: CVComponentKey { .bodyMedia }

    private let bodyMedia: CVComponentState.BodyMedia
    private var items: [CVMediaAlbumItem] {
        bodyMedia.items
    }

    private var areAllItemsImages: Bool {
        return items.allSatisfy {
            switch $0.attachment.contentType {
            case .image: true
            case .video, .audio, .file: false
            }
        }
    }

    private let footerOverlay: CVComponent?

    init(itemModel: CVItemModel, bodyMedia: CVComponentState.BodyMedia, footerOverlay: CVComponent?) {
        self.bodyMedia = bodyMedia
        self.footerOverlay = footerOverlay

        super.init(itemModel: itemModel)
    }

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

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

        let conversationStyle = self.conversationStyle

        let albumView = componentView.albumView
        albumView.configure(
            mediaCache: mediaCache,
            items: items,
            interaction: interaction,
            isBorderless: isBorderless,
            cellMeasurement: cellMeasurement,
            conversationStyle: conversationStyle,
        )

        let stackView = componentView.stackView

        stackView.reset()
        stackView.configure(
            config: stackConfig,
            cellMeasurement: cellMeasurement,
            measurementKey: Self.measurementKey_stackView,
            subviews: [albumView],
        )

        if let 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
            stackView.addSubview(footerRootView)
            let footerSize = cellMeasurement.size(key: Self.measurementKey_footerSize) ?? .zero
            stackView.addLayoutBlock { view in
                var footerFrame = view.bounds
                // Apply h-insets.
                footerFrame.x += conversationStyle.textInsetHorizontal
                footerFrame.width -= conversationStyle.textInsetHorizontal * 2
                // Ensure footer height fits within text insets.
                let maxFooterHeight = (
                    view.bounds.height -
                        (conversationStyle.textInsetTop + conversationStyle.textInsetBottom),
                )
                footerFrame.height = min(maxFooterHeight, footerSize.height)
                // Bottom align.
                footerFrame.y = (
                    view.bounds.height -
                        (
                            footerFrame.height +
                                conversationStyle.textInsetBottom
                        ),
                )
                footerRootView.frame = footerFrame
            }

            let maxGradientHeight: CGFloat = 40
            let gradientLayer = CAGradientLayer()
            gradientLayer.colors = [
                UIColor(white: 0, alpha: 0.0).cgColor,
                UIColor(white: 0, alpha: 0.4).cgColor,
            ]
            let gradientView = OWSLayerView(frame: .zero) { layerView in
                var layerFrame = layerView.bounds
                layerFrame.height = min(maxGradientHeight, layerView.height)
                layerFrame.y = layerView.height - layerFrame.height
                gradientLayer.frame = layerFrame
            }
            componentView.bodyMediaGradientView = gradientView
            gradientView.layer.addSublayer(gradientLayer)
            albumView.addSubview(gradientView)
            stackView.layoutSubviewToFillSuperviewEdges(gradientView)
        }

        // Only apply "inner shadow" for single media, not albums.
        if
            !isBorderless,
            albumView.itemViews.count == 1,
            let firstMediaView = albumView.itemViews.first
        {
            let shadowColor: UIColor = isDarkThemeEnabled ? .white : .black
            let innerShadowView = OWSBubbleShapeView(mode: .innerShadow(
                color: shadowColor,
                radius: 0.5,
                opacity: 0.15,
            ))
            componentView.innerShadowView = innerShadowView
            firstMediaView.addSubview(innerShadowView)
            stackView.layoutSubviewToFillSuperviewEdges(innerShadowView)
        }

        if bodyMedia.mediaAlbumHasSkippedAttachment {
            // Media size label and download icon should both use the same color that CVAttachmentProgressView uses.
            let backgroundCircleConfiguration = CVAttachmentProgressView.Configuration.forMediaOverlay()

            let iconViewSize = CGSize.square(24)
            let iconView = CVImageView(image: Theme.iconImage(.arrowDown))
            iconView.tintColor = backgroundCircleConfiguration.foregroundColor

            if albumView.itemViews.count > 1 {
                // Download icon and number of media displayed over pill-shaped blur background.

                let downloadStackConfig = ManualStackView.Config(
                    axis: .horizontal,
                    alignment: .center,
                    spacing: 6,
                    layoutMargins: UIEdgeInsets(top: 8, leading: 10, bottom: 8, trailing: 14),
                )
                let downloadStack = ManualStackView(name: "downloadStack")
                downloadStack.apply(config: downloadStackConfig)
                var subviewInfos = [ManualStackSubviewInfo]()

                let pillBackgroundView = CVAttachmentProgressView.circularBackgroundView(configuration: backgroundCircleConfiguration)
                downloadStack.addSubviewToFillSuperviewEdges(pillBackgroundView)

                downloadStack.addArrangedSubview(iconView)
                subviewInfos.append(iconViewSize.asManualSubviewInfo(hasFixedSize: true))

                let downloadLabel = CVLabel()
                let downloadFormat = (
                    areAllItemsImages
                        ? OWSLocalizedString(
                            "MEDIA_GALLERY_ITEM_IMAGE_COUNT_%d",
                            tableName: "PluralAware",
                            comment: "Format for an indicator of the number of image items in a media gallery. Embeds {{ the number of items in the media gallery }}.",
                        )
                        : OWSLocalizedString(
                            "MEDIA_GALLERY_ITEM_MIXED_COUNT_%d",
                            tableName: "PluralAware",
                            comment: "Format for an indicator of the number of image or video items in a media gallery. Embeds {{ the number of items in the media gallery }}.",
                        ),
                )
                downloadStack.addArrangedSubview(downloadLabel)
                let downloadLabelConfig = CVLabelConfig(
                    text: .text(String.localizedStringWithFormat(downloadFormat, items.count)),
                    displayConfig: .forUnstyledText(
                        font: .dynamicTypeSubheadline,
                        textColor: backgroundCircleConfiguration.foregroundColor,
                    ),
                    font: .dynamicTypeSubheadline,
                    textColor: backgroundCircleConfiguration.foregroundColor,
                )
                downloadLabelConfig.applyForRendering(label: downloadLabel)
                let downloadLabelSize = CVText.measureLabel(
                    config: downloadLabelConfig,
                    maxWidth: CGFloat.greatestFiniteMagnitude,
                )
                subviewInfos.append(downloadLabelSize.asManualSubviewInfo)

                let downloadStackMeasurement = ManualStackView.measure(
                    config: downloadStackConfig,
                    subviewInfos: subviewInfos,
                )
                downloadStack.measurement = downloadStackMeasurement
                stackView.addSubviewToCenterOnSuperview(
                    downloadStack,
                    size: downloadStackMeasurement.measuredSize,
                )
            } else {
                // Just an icon over circular blur background.
                let circleSize = CGSize.square(44)
                let circleView = CVAttachmentProgressView.circularBackgroundView(configuration: backgroundCircleConfiguration)
                stackView.addSubviewToCenterOnSuperview(circleView, size: circleSize)
                stackView.addSubviewToCenterOnSuperview(iconView, size: iconViewSize)
            }

            if bodyMedia.mediaAlbumHasSkippedAttachment {
                let pendingManualDownloadAttachments = items
                    .lazy
                    .compactMap { (item: CVMediaAlbumItem) -> ReferencedAttachment? in
                        switch item.attachment {
                        case .stream:
                            return nil
                        case .backupThumbnail:
                            // TODO:[Backups]: Check for media tier download state
                            return nil
                        case .pointer(let attachment, let downloadState):
                            if item.threadHasPendingMessageRequest {
                                // Doesn't count.
                                return nil
                            }
                            switch downloadState {
                            case .none:
                                return attachment
                            case .enqueuedOrDownloading, .failed:
                                return nil
                            }
                        case .undownloadable:
                            return nil
                        }
                    }
                let totalSize = pendingManualDownloadAttachments.map {
                    $0.attachment.asAnyPointer()?.unencryptedByteCount ?? 0
                }.reduce(0, +)

                // Total size of undownloaded media displayed over the blur pill-shaped background.
                if totalSize > 0 {
                    var downloadSizeText = [OWSFormat.localizedFileSizeString(from: UInt64(safeCast: totalSize))]
                    if
                        pendingManualDownloadAttachments.count == 1,
                        let firstAttachmentPointer = pendingManualDownloadAttachments.first
                    {
                        let mimeType = firstAttachmentPointer.attachment.mimeType
                        if
                            MimeTypeUtil.isSupportedDefinitelyAnimatedMimeType(mimeType)
                            || firstAttachmentPointer.reference.renderingFlag == .shouldLoop
                        {
                            // Do nothing.
                        } else if MimeTypeUtil.isSupportedImageMimeType(mimeType) {
                            downloadSizeText.append(CommonStrings.attachmentTypePhoto)
                        } else if MimeTypeUtil.isSupportedVideoMimeType(mimeType) {
                            downloadSizeText.append(CommonStrings.attachmentTypeVideo)
                        }
                    }

                    let downloadSizeView = ManualLayoutViewWithLayer.pillView(name: "downloadSizeView")
                    downloadSizeView.layoutMargins = UIEdgeInsets(hMargin: 8, vMargin: 4)

                    let pillBackgroundView = CVAttachmentProgressView.circularBackgroundView(configuration: backgroundCircleConfiguration)
                    downloadSizeView.addSubviewToFillSuperviewEdges(pillBackgroundView)

                    let downloadSizeLabelConfig = CVLabelConfig(
                        text: .text(downloadSizeText.joined(separator: " • ")),
                        displayConfig: .forUnstyledText(
                            font: .dynamicTypeCaption1,
                            textColor: backgroundCircleConfiguration.foregroundColor,
                        ),
                        font: .dynamicTypeCaption1,
                        textColor: backgroundCircleConfiguration.foregroundColor,
                    )
                    let downloadSizeLabel = CVLabel()
                    downloadSizeLabelConfig.applyForRendering(label: downloadSizeLabel)
                    let downloadSizeLabelSize = CVText.measureLabel(
                        config: downloadSizeLabelConfig,
                        maxWidth: .greatestFiniteMagnitude,
                    )
                    downloadSizeView.addSubviewToFillSuperviewMargins(downloadSizeLabel)

                    let downloadSizeViewSize = downloadSizeLabelSize + downloadSizeView.layoutMargins.asSize
                    stackView.addSubview(downloadSizeView)
                    stackView.addLayoutBlock { view in
                        let inset: CGFloat = 6
                        let x = (
                            CurrentAppContext().isRTL
                                ? view.width - (downloadSizeViewSize.width - inset)
                                : inset,
                        )
                        downloadSizeView.frame = CGRect(
                            x: x,
                            y: inset,
                            width: downloadSizeViewSize.width,
                            height: downloadSizeViewSize.height,
                        )
                    }
                }
            }
        }
    }

    func bubbleViewPartner(componentView: CVComponentView) -> OWSBubbleViewPartner? {
        guard let componentView = componentView as? CVComponentViewBodyMedia else {
            owsFailDebug("Unexpected componentView.")
            return nil
        }
        return componentView.innerShadowView
    }

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

    private var maxMediaMessageWidth: CGFloat {
        let maxMediaMessageWidth = conversationStyle.maxMediaMessageWidth
        if self.isBorderless {
            return min(175, maxMediaMessageWidth)
        }
        return maxMediaMessageWidth
    }

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

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

        // We may need to reserve space for a footer overlay.
        var minWidth: CGFloat = 0
        if let footerOverlay = self.footerOverlay {
            let maxFooterWidth = max(0, maxWidth - conversationStyle.textInsets.totalWidth)
            let footerSize = footerOverlay.measure(
                maxWidth: maxFooterWidth,
                measurementBuilder: measurementBuilder,
            )
            minWidth = min(maxWidth, footerSize.width + conversationStyle.textInsets.totalWidth)
            measurementBuilder.setSize(key: Self.measurementKey_footerSize, size: footerSize)
        }

        let maxWidth = min(maxWidth, maxMediaMessageWidth)

        let albumSize = CVMediaAlbumView.measure(
            maxWidth: maxWidth,
            minWidth: minWidth,
            items: self.items,
            measurementBuilder: measurementBuilder,
        )
        let albumInfo = albumSize.asManualSubviewInfo
        let stackMeasurement = ManualStackView.measure(
            config: stackConfig,
            measurementBuilder: measurementBuilder,
            measurementKey: Self.measurementKey_stackView,
            subviewInfos: [albumInfo],
            maxWidth: maxWidth,
        )
        return stackMeasurement.measuredSize
    }

    // MARK: - Events

    override func cellWillBecomeVisible(
        componentDelegate: CVComponentDelegate,
    ) {
        AssertIsOnMainThread()

        if
            let message = interaction as? TSMessage,
            bodyMedia.mediaAlbumHasFailedAttachment || bodyMedia.mediaAlbumHasSkippedAttachment
        {
            componentDelegate.willBecomeVisibleWithSkippedDownloads(message)
        }
    }

    override func handleTap(
        sender: UIGestureRecognizer,
        componentDelegate: CVComponentDelegate,
        componentView: CVComponentView,
        renderItem: CVRenderItem,
    ) -> Bool {
        AssertIsOnMainThread()

        guard let componentView = componentView as? CVComponentViewBodyMedia else {
            owsFailDebug("Unexpected componentView.")
            return false
        }
        guard let message = interaction as? TSMessage else {
            owsFailDebug("Invalid interaction.")
            return false
        }

        if bodyMedia.mediaAlbumHasSkippedAttachment {
            componentDelegate.didTapSkippedDownloads(message)
            return true
        }

        let albumView = componentView.albumView
        let location = sender.location(in: albumView)
        guard let mediaView = albumView.mediaView(forLocation: location) else {
            Logger.warn("Missing mediaView.")
            return false
        }

        if
            albumView.isMoreItemsView(mediaView: mediaView),
            bodyMedia.mediaAlbumHasFailedAttachment
        {
            componentDelegate.didTapSkippedDownloads(message)
            return true
        }

        switch mediaView.attachment {
        case .pointer(let pointer, let downloadState):
            switch downloadState {
            case .failed, .none:
                componentDelegate.didTapSkippedDownloads(message)
                return true
            case .enqueuedOrDownloading:
                componentDelegate.didCancelDownload(message, attachmentId: pointer.attachment.id)
                return true
            }
        case .stream(let referencedAttachmentStream, isUploading: _, imageMetadata: _):
            let itemViewModel = CVItemViewModelImpl(renderItem: renderItem)
            if let item = items.first(where: { $0.attachment.attachment.attachment.id == referencedAttachmentStream.attachment.id }), item.isBroken {
                componentDelegate.didTapBrokenVideo()
                return true
            }
            componentDelegate.didTapBodyMedia(
                itemViewModel: itemViewModel,
                attachment: referencedAttachmentStream,
                imageView: mediaView,
            )
            return true
        case .backupThumbnail(let thumbnail):
            let itemViewModel = CVItemViewModelImpl(renderItem: renderItem)
            componentDelegate.didTapBodyMedia(
                itemViewModel: itemViewModel,
                attachment: thumbnail,
                imageView: mediaView,
            )
            return true
        case .undownloadable:
            componentDelegate.didTapUndownloadableMedia()
            return true
        }
    }

    func albumItemView(
        forAttachment attachment: ReferencedAttachment,
        componentView: CVComponentView,
    ) -> UIView? {
        guard let componentView = componentView as? CVComponentViewBodyMedia else {
            owsFailDebug("Unexpected componentView.")
            return nil
        }
        let albumView = componentView.albumView
        guard
            let albumItemView = (albumView.itemViews.first {
                $0.attachment.attachment.attachment.id == attachment.attachment.id
                    && $0.attachment.attachment.reference.hasSameOwner(as: attachment.reference)
            })
        else {
            assert(albumView.moreItemsView != nil)
            return albumView.moreItemsView
        }
        return albumItemView
    }

    // MARK: -

    // We use this view to implement BodyMediaPresentationContext below.
    class CVComponentViewBodyMediaRootView: ManualStackView {

        fileprivate var bodyMediaGradientView: UIView?

        fileprivate var footerOverlayView: CVComponentView?

        override open func reset() {
            bodyMediaGradientView = nil
            footerOverlayView = nil

            super.reset()
        }
    }

    // MARK: -

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

        fileprivate let stackView = CVComponentViewBodyMediaRootView(name: "stackView")

        fileprivate let albumView = CVMediaAlbumView()

        fileprivate var bodyMediaGradientView: UIView? {
            get { stackView.bodyMediaGradientView }
            set { stackView.bodyMediaGradientView = newValue }
        }

        fileprivate var innerShadowView: OWSBubbleShapeView?

        var isDedicatedCellView = false

        var rootView: UIView {
            stackView
        }

        // MARK: - Subcomponents

        fileprivate var footerOverlayView: CVComponentView? {
            get { stackView.footerOverlayView }
            set { stackView.footerOverlayView = newValue }
        }

        // MARK: -

        func setIsCellVisible(_ isCellVisible: Bool) {
            if isCellVisible {
                albumView.loadMedia()
            } else {
                albumView.unloadMedia()
            }
        }

        func reset() {
            albumView.reset()
            stackView.reset()
            footerOverlayView?.reset()

            bodyMediaGradientView?.removeFromSuperview()
            bodyMediaGradientView = nil

            innerShadowView?.removeFromSuperview()
            innerShadowView = nil
        }

    }
}

// MARK: -

protocol BodyMediaPresentationContext {
    var mediaOverlayViews: [UIView] { get }
}

// MARK: -

extension CVComponentBodyMedia.CVComponentViewBodyMediaRootView: BodyMediaPresentationContext {
    var mediaOverlayViews: [UIView] {
        var result = [UIView]()
        if let footerOverlayView {
            result.append(footerOverlayView.rootView)
        }
        if let bodyMediaGradientView {
            result.append(bodyMediaGradientView)
        }
        return result
    }
}

// MARK: -

extension CVComponentBodyMedia: CVAccessibilityComponent {
    var accessibilityDescription: String {
        let genericMediaString = OWSLocalizedString(
            "ACCESSIBILITY_LABEL_MEDIA",
            comment: "Accessibility label for media.",
        )

        if bodyMedia.items.count > 1 {
            return String.localizedStringWithFormat(
                OWSLocalizedString(
                    "ACCESSIBILITY_LABEL_MULTIPLE_ATTACHMENTS_%d",
                    tableName: "PluralAware",
                    comment: "Accessibility label for multiple attachment items. Embeds {{ number of attachments }}.",
                ),
                bodyMedia.items.count,
            )
        }

        guard let mediaItem = bodyMedia.items.first else {
            return genericMediaString
        }

        let contentType: Attachment.ContentType
        let imageMetadata: ImageMetadata?
        switch mediaItem.attachment {
        case .stream(let referencedAttachmentStream, isUploading: _, let _imageMetadata):
            contentType = referencedAttachmentStream.attachment.contentType
            imageMetadata = _imageMetadata
        case .pointer(let referencedAttachmentPointer, downloadState: _):
            contentType = referencedAttachmentPointer.attachment.contentType
            imageMetadata = nil
        case .backupThumbnail(let referencedAttachmentBackupThumbnail):
            contentType = referencedAttachmentBackupThumbnail.attachment.contentType
            imageMetadata = nil
        case .undownloadable(let referencedAttachment):
            contentType = referencedAttachment.attachment.contentType
            imageMetadata = nil
        }

        switch contentType {
        case .file:
            return CommonStrings.attachmentTypeFile
        case .image:
            if let imageMetadata, imageMetadata.isAnimated {
                return CommonStrings.attachmentTypeAnimated
            } else {
                return CommonStrings.attachmentTypePhoto
            }
        case .video:
            if mediaItem.renderingFlag == .shouldLoop {
                return CommonStrings.attachmentTypeAnimated
            } else {
                return CommonStrings.attachmentTypeVideo
            }
        case .audio:
            return CommonStrings.attachmentTypeAudio
        }
    }
}
