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

import SignalServiceKit
import SignalUI

enum ViewOnceState: Equatable {
    case unknown
    case incomingExpired
    case incomingDownloading(attachmentPointer: TSResourcePointer, renderingFlag: AttachmentReference.RenderingFlag)
    case incomingFailed
    case incomingPending
    case incomingAvailable(attachmentStream: TSResourceStream, renderingFlag: AttachmentReference.RenderingFlag)
    case incomingInvalidContent
    case outgoingSending
    case outgoingFailed
    case outgoingSentExpired

    static func == (lhs: ViewOnceState, rhs: ViewOnceState) -> Bool {
        switch (lhs, rhs) {
        case
            (.unknown, .unknown),
            (.incomingExpired, .incomingExpired),
            (.incomingFailed, .incomingFailed),
            (.incomingPending, .incomingPending),
            (.incomingInvalidContent, .incomingInvalidContent),
            (.outgoingSending, .outgoingSending),
            (.outgoingFailed, .outgoingFailed),
            (.outgoingSentExpired, .outgoingSentExpired):
            return true
        case let (.incomingDownloading(lhsPointer, lhsFlag), .incomingDownloading(rhsPointer, rhsFlag)):
            return lhsPointer.resourceId == rhsPointer.resourceId
                && lhsFlag == rhsFlag
        case let (.incomingAvailable(lhsStream, lhsFlag), .incomingAvailable(rhsStream, rhsFlag)):
            return lhsStream.resourceId == rhsStream.resourceId
                && lhsFlag == rhsFlag
        case
            (.unknown, _),
            (.incomingExpired, _),
            (.incomingFailed, _),
            (.incomingPending, _),
            (.incomingInvalidContent, _),
            (.outgoingSending, _),
            (.outgoingFailed, _),
            (.outgoingSentExpired, _),
            (.incomingDownloading, _),
            (.incomingAvailable, _):
            return false
        }
    }
}

// MARK: -

public class CVComponentViewOnce: CVComponentBase, CVComponent {

    public var componentKey: CVComponentKey { .viewOnce }

    private enum ViewOnceMessageType: Equatable {
        case unknown
        case photo
        case video
    }

    // MARK: -

    private let viewOnce: CVComponentState.ViewOnce
    private var viewOnceState: ViewOnceState {
        viewOnce.viewOnceState
    }
    private var isExpired: Bool {
        switch viewOnce.viewOnceState {
        case .incomingExpired, .outgoingSentExpired:
            return true
        default:
            return false
        }
    }
    private var attachmentStream: TSResourceStream? {
        if case .incomingAvailable(let attachmentStream, _) = viewOnceState {
            return attachmentStream
        }
        return nil
    }
    private var shouldShowIcon: Bool {
        switch viewOnceState {
        case .incomingInvalidContent, .incomingDownloading:
            return false
        default:
            return true
        }
    }
    private var shouldShowProgress: Bool {
        switch viewOnceState {
        case .incomingDownloading:
            return true
        default:
            return false
        }
    }

    init(itemModel: CVItemModel, viewOnce: CVComponentState.ViewOnce) {
        self.viewOnce = viewOnce

        super.init(itemModel: itemModel)
    }

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

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

        var subviews = [UIView]()

        switch viewOnceState {
        case .incomingDownloading(let attachmentPointer, _):
            let progressView = CVAttachmentProgressView(
                direction: .download(
                    attachmentPointer: attachmentPointer,
                    transitTierDownloadState: .downloading
                ),
                diameter: iconSize,
                isDarkThemeEnabled: conversationStyle.isDarkThemeEnabled,
                mediaCache: mediaCache
            )
            subviews.append(progressView)
        default:
            if shouldShowIcon, let iconName = self.iconName {
                let iconView = componentView.iconView
                iconView.setTemplateImageName(iconName, tintColor: iconColor)
                subviews.append(iconView)
            }
        }

        let label = componentView.label
        labelConfig.applyForRendering(label: label)
        subviews.append(label)

        let stackView = componentView.stackView
        stackView.reset()
        stackView.configure(config: stackViewConfig,
                            cellMeasurement: cellMeasurement,
                            measurementKey: Self.measurementKey_stackView,
                            subviews: subviews)
    }

    private let iconSize: CGFloat = 24

    private var downloadProgressRadius: CGFloat {
        iconSize * 0.5
    }

    private var stackViewConfig: CVStackViewConfig {
        CVStackViewConfig(axis: .horizontal,
                          alignment: .center,
                          spacing: 8,
                          layoutMargins: .zero)
    }

    private static let measurementKey_stackView = "CVComponentViewOnce.measurementKey_stackView"

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

        var subviewInfos = [ManualStackSubviewInfo]()

        let hasIcon = shouldShowIcon && iconName != nil
        let hasIconOrProgress = hasIcon || shouldShowProgress

        var availableWidth = maxWidth
        if hasIconOrProgress {
            availableWidth = max(0, availableWidth - (iconSize + stackViewConfig.spacing))
            subviewInfos.append(CGSize.square(iconSize).asManualSubviewInfo(hasFixedSize: true))
        }
        let textSize = CVText.measureLabel(config: labelConfig, maxWidth: availableWidth)
        subviewInfos.append(textSize.asManualSubviewInfo)

        let stackMeasurement = ManualStackView.measure(config: stackViewConfig,
                                                       measurementBuilder: measurementBuilder,
                                                       measurementKey: Self.measurementKey_stackView,
                                                       subviewInfos: subviewInfos,
                                                       maxWidth: maxWidth)
        var result = stackMeasurement.measuredSize
        // We use this "min width" to reduce/avoid "flutter"
        // in the bubble's size as the message changes states.
        let minContentWidth: CGFloat = maxWidth * 0.4
        result.width = max(result.width, minContentWidth)
        return result
    }

    // MARK: - Events

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

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

        switch viewOnceState {
        case .unknown:
            owsFailDebug("Invalid value.")
        case .incomingDownloading,
             .incomingInvalidContent:
            break
        case .incomingFailed, .incomingPending:
            componentDelegate.didTapFailedOrPendingDownloads(message)
        case .incomingAvailable:
            componentDelegate.didTapViewOnceAttachment(message)
        case .incomingExpired, .outgoingSentExpired:
            componentDelegate.didTapViewOnceExpired(message)
        case .outgoingFailed,
             .outgoingSending:
            break
        }
        return true
    }

    // MARK: -

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

        fileprivate let stackView = ManualStackView(name: "viewOnce")
        fileprivate let iconView = CVImageView()
        fileprivate let label = CVLabel()

        public var isDedicatedCellView = false

        public var rootView: UIView {
            stackView
        }

        public func setIsCellVisible(_ isCellVisible: Bool) {}

        public func reset() {
            stackView.reset()
            iconView.image = nil
            label.text = nil
        }

    }
}

// MARK: -

fileprivate extension CVComponentViewOnce {
    var iconName: String? {
        switch viewOnceState {
        case .unknown:
            owsFailDebug("Invalid value.")
            return "view_once-dash"
        case .incomingExpired:
            return "view_once-dash"
        case .incomingDownloading:
            owsFailDebug("Unexpected state.")
            return nil
        case .incomingFailed, .incomingPending:
            return "arrow-circle-down"
        case .incomingAvailable:
            return "view_once"
        case .outgoingFailed:
            return "refresh"
        case .outgoingSending,
             .outgoingSentExpired:
            return "view_once-dash"
        case .incomingInvalidContent:
            owsFailDebug("Unexpected state.")
            return nil
        }
    }

    var textColor: UIColor {
        switch viewOnceState {
        case .unknown:
            owsFailDebug("Invalid value.")
            return conversationStyle.bubbleTextColorIncoming
        case .incomingExpired,
             .incomingDownloading,
             .incomingFailed,
             .incomingPending,
             .incomingAvailable:
            return conversationStyle.bubbleTextColorIncoming
        case .outgoingFailed,
             .outgoingSending,
             .outgoingSentExpired:
            return conversationStyle.bubbleTextColorOutgoing
        case .incomingInvalidContent:
            return Theme.secondaryTextAndIconColor
        }
    }

    var iconColor: UIColor {
        let pendingColor: UIColor = (Theme.isDarkThemeEnabled ? .ows_gray15 : .ows_gray75)

        switch viewOnceState {
        case .unknown:
            owsFailDebug("Invalid value.")
            return conversationStyle.bubbleTextColorIncoming
        case .incomingExpired:
            return conversationStyle.bubbleTextColorIncoming
        case .incomingDownloading,
             .incomingFailed,
             .incomingPending:
            return pendingColor
        case .incomingAvailable:
            return conversationStyle.bubbleTextColorIncoming
        case .outgoingFailed:
            return pendingColor
        case .outgoingSending,
             .outgoingSentExpired:
            return conversationStyle.bubbleTextColorOutgoing
        case .incomingInvalidContent:
            return Theme.secondaryTextAndIconColor
        }
    }

    var labelConfig: CVLabelConfig {
        func buildDefaultConfig(text: String) -> CVLabelConfig {
            return CVLabelConfig.unstyledText(
                text,
                font: UIFont.dynamicTypeSubheadline.semibold(),
                textColor: textColor,
                numberOfLines: 1,
                lineBreakMode: .byTruncatingTail
            )
        }

        switch viewOnceState {
        case .unknown:
            owsFailDebug("Invalid value.")
            return buildDefaultConfig(text: CommonStrings.genericError)
        case .incomingExpired:
            let text = OWSLocalizedString("PER_MESSAGE_EXPIRATION_VIEWED",
                                         comment: "Label for view-once messages indicating that the local user has viewed the message's contents.")
            return buildDefaultConfig(text: text)
        case .incomingDownloading:
            let text = OWSLocalizedString("MESSAGE_STATUS_DOWNLOADING", comment: "message status while message is downloading.")
            return buildDefaultConfig(text: text)
        case .incomingFailed:
            let text = CommonStrings.retryButton
            return buildDefaultConfig(text: text)
        case .incomingPending:
            let text = OWSLocalizedString("ACTION_TAP_TO_DOWNLOAD", comment: "A label for 'tap to download' buttons.")
            return buildDefaultConfig(text: text)
        case .incomingAvailable:
            let text: String
            switch viewOnceMessageType {
            case .photo:
                text = MessageStrings.viewOnceViewPhoto
            case .video:
                text = MessageStrings.viewOnceViewVideo
            case .unknown:
                owsFailDebug("unexpected viewOnceMessageType for IncomingFailed.")
                text = MessageStrings.viewOnceViewPhoto
            }
            return buildDefaultConfig(text: text)
        case .outgoingFailed:
            let text = CommonStrings.retryButton
            return buildDefaultConfig(text: text)
        case .outgoingSending,
             .outgoingSentExpired:
            let text = OWSLocalizedString(
                "PER_MESSAGE_EXPIRATION_OUTGOING_MESSAGE", comment: "Label for outgoing view-once messages.")
            return buildDefaultConfig(text: text)
        case .incomingInvalidContent:
            let text = OWSLocalizedString(
                "PER_MESSAGE_EXPIRATION_INVALID_CONTENT", comment: "Label for view-once messages that have invalid content.")
            // Reconfigure label for this state only.
            return CVLabelConfig.unstyledText(
                text,
                font: UIFont.dynamicTypeSubheadline,
                textColor: Theme.secondaryTextAndIconColor,
                numberOfLines: 0,
                lineBreakMode: .byWordWrapping
            )
        }
    }

    private var viewOnceMessageType: ViewOnceMessageType {
        switch viewOnceState {
        case let .incomingAvailable(attachmentStream, _):
            switch attachmentStream.cachedContentType {
            case .file, .invalid, .audio:
                owsFailDebug("Invalid view once type")
                return .unknown
            case .image:
                return .photo
            case .video:
                return .video
            case .animatedImage:
                return .photo
            case nil:
                let mimeType = attachmentStream.mimeType
                if MimeTypeUtil.isSupportedVideoMimeType(mimeType) {
                    return .video
                } else {
                    owsAssertDebug(
                        MimeTypeUtil.isSupportedImageMimeType(mimeType)
                        || MimeTypeUtil.isSupportedMaybeAnimatedMimeType(mimeType)
                    )
                    return .photo
                }
            }
        case .unknown,
             .incomingExpired,
             .incomingDownloading,
             .incomingFailed,
             .incomingPending,
             .incomingInvalidContent,
             .outgoingSending,
             .outgoingFailed,
             .outgoingSentExpired:
            // The attachment doesn't exist for outgoing
            // messages so we'd need to store the content type if
            // we wanted to distinguish between photo and video

            // For incoming messages viewed messages, it doesn't matter
            // because we show generic "View" text, regardless of the
            // content type
            return .unknown
        }
    }
}

// MARK: -

extension CVComponentViewOnce: CVAccessibilityComponent {
    public var accessibilityDescription: String {
        // TODO: We could include the media type (video, image, animated image).
        labelConfig.text.accessibilityDescription
    }
}
