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

import SignalServiceKit
import LibSignalClient

/// View model for an existing quoted reply which has already fetched any attachments.
/// NOT used for draft quoted replies; this is for TSMessages with quoted replies (or story replies)
/// that have already been created, for use rendering in a conversation.
public class QuotedReplyModel {

    /// Timestamp of the original message, be it StoryMessage or TSMessage.
    public let originalMessageTimestamp: UInt64?

    /// Address of the original message's author, be it StoryMessage or TSMessage.
    public let originalMessageAuthorAddress: SignalServiceAddress

    public let isOriginalMessageAuthorLocalUser: Bool

    /// IFF the original's content was a story message, the emoji used
    /// _on the reply body_ to that story message.
    /// Ignored for other original content types.
    public let storyReactionEmoji: String?

    /// The content on the _original_ message being replied to.
    public enum OriginalContent {

        /// The original message had text with no attachment
        case text(MessageBody?)

        // MARK: - "Special" types

        /// The original message was a gift badge
        case giftBadge
        /// The original message is itself a reply to a story
        /// with an emoji.
        case storyReactionEmoji(String)

        // MARK: - Attachment types

        /// The original message had an attachment, but it could not
        /// be thumbnail-ed
        case attachmentStub(
            MessageBody?,
            QuotedMessageAttachmentReference.Stub
        )
        /// The original message had an attachment that can be thumbnail-ed,
        /// though it may not actually be thumbnail-ed *yet*.
        case attachment(
            MessageBody?,
            attachment: ReferencedTSResource,
            thumbnailImage: UIImage?
        )

        // MARK: - Story types

        case mediaStory(
            body: StyleOnlyMessageBody?,
            attachment: ReferencedTSResource,
            thumbnailImage: UIImage?
        )

        public typealias TextStoryThumbnailRenderer = (SpoilerRenderState) -> UIView
        case textStory(TextStoryThumbnailRenderer)

        /// Used if the story has expired; we do not retain a copy.
        case expiredStory

        // MARK: - Convenience

        public var isGiftBadge: Bool {
            switch self {
            case .giftBadge:
                return true
            default:
                return false
            }
        }

        public var isStory: Bool {
            switch self {
            case .mediaStory, .textStory, .expiredStory:
                return true
            default:
                return false
            }
        }

        public var attachmentMimeType: String? {
            switch self {
            case .text(_):
                return nil
            case .giftBadge:
                return nil
            case .storyReactionEmoji(_):
                return nil
            case .attachmentStub(_, let stub):
                return stub.mimeType
            case .attachment(_, let attachment, _):
                return attachment.attachment.mimeType
            case .mediaStory(_, let attachment, _):
                return attachment.attachment.mimeType
            case .textStory(_):
                return nil
            case .expiredStory:
                return nil
            }
        }

        public var attachmentCachedContentType: TSResourceContentType? {
            switch self {
            case .text(_):
                return nil
            case .giftBadge:
                return nil
            case .storyReactionEmoji(_):
                return nil
            case .attachmentStub(_, _):
                return nil
            case .attachment(_, let attachment, _):
                return attachment.attachment.asResourceStream()?.cachedContentType
            case .mediaStory(_, let attachment, _):
                return attachment.attachment.asResourceStream()?.cachedContentType
            case .textStory(_):
                return nil
            case .expiredStory:
                return nil
            }
        }
    }

    public let originalContent: OriginalContent

    /// Where we got the origina's content from.
    /// In plain english: did we have the original message (NOT its attachment,
    /// the TSMessage itself) locally when we created the TSQuotedMessage?
    public let sourceOfOriginal: TSQuotedMessageContentSource

    // MARK: Convenience

    public var originalMessageBody: MessageBody? {
        switch originalContent {
        case .text(let messageBody):
            return messageBody
        case .giftBadge:
            return nil
        case .storyReactionEmoji(let string):
            return MessageBody(text: string, ranges: .empty)
        case .attachmentStub(let messageBody, _):
            return messageBody
        case .attachment(let messageBody, _, _):
            return messageBody
        case .mediaStory(let body, _, _):
            return body?.asMessageBody()
        case .textStory(_):
            return nil
        case .expiredStory:
            return MessageBody(
                text: OWSLocalizedString(
                    "STORY_NO_LONGER_AVAILABLE",
                    comment: "Text indicating a story that was replied to is no longer available."
                ),
                ranges: .empty
            )
        }
    }

    public var originalAttachmentSourceFilename: String? {
        switch originalContent {
        case .text(_):
            return nil
        case .giftBadge:
            return nil
        case .storyReactionEmoji(_):
            return nil
        case .attachmentStub(_, let stub):
            return stub.sourceFilename
        case .attachment(_, let attachment, _):
            return attachment.reference.sourceFilename
        case .mediaStory(_, _, _):
            return nil
        case .textStory(_):
            return nil
        case .expiredStory:
            return nil
        }
    }

    public var hasQuotedThumbnail: Bool {
        switch originalContent {
        case .text(_):
            return false
        case .giftBadge:
            // This pretends to be a thumbnail
            return true
        case .storyReactionEmoji(_):
            return false
        case .attachmentStub(_, _):
            return false
        case .attachment(_, _, let thumbnailImage):
            return thumbnailImage != nil
        case .mediaStory(_, _, _):
            return true
        case .textStory(_):
            return true
        case .expiredStory:
            return false
        }
    }

    public static func build(
        replyingTo storyMessage: StoryMessage,
        reactionEmoji: String? = nil,
        transaction: SDSAnyReadTransaction
    ) -> QuotedReplyModel {
        let isOriginalAuthorLocalUser = DependenciesBridge.shared.tsAccountManager
            .localIdentifiers(tx: transaction.asV2Read)?
            .aciAddress
            .isEqualToAddress(storyMessage.authorAddress)
            ?? false

        func buildQuotedReplyModel(
            originalContent: OriginalContent
        ) -> QuotedReplyModel {
            return QuotedReplyModel(
                originalMessageTimestamp: storyMessage.timestamp,
                originalMessageAuthorAddress: storyMessage.authorAddress,
                isOriginalMessageAuthorLocalUser: isOriginalAuthorLocalUser,
                storyReactionEmoji: reactionEmoji,
                originalContent: originalContent,
                sourceOfOriginal: .story
            )
        }

        switch storyMessage.attachment {
        case .file, .foreignReferenceAttachment:
            let attachmentReference = DependenciesBridge.shared.tsResourceStore.mediaAttachment(for: storyMessage, tx: transaction.asV2Read)
            let attachment = attachmentReference?.fetch(tx: transaction)

            let referencedAttachment: ReferencedTSResource
            let thumbnailImage: UIImage?
            if let attachmentReference, let attachment {
                referencedAttachment = .init(reference: attachmentReference, attachment: attachment)

                if let stream = attachment.asResourceStream() {
                    thumbnailImage = stream.thumbnailImageSync(quality: .small)
                } else if let blurHash = attachment.resourceBlurHash {
                    thumbnailImage = BlurHash.image(for: blurHash)
                } else {
                    thumbnailImage = nil
                }
            } else {
                return buildQuotedReplyModel(originalContent: .expiredStory)
            }
            return buildQuotedReplyModel(originalContent: .mediaStory(
                body: attachmentReference?.storyMediaCaption,
                attachment: referencedAttachment,
                thumbnailImage: thumbnailImage
            ))

        case .text(let textAttachment):
            let preloadedTextAttachment = PreloadedTextAttachment.from(
                textAttachment,
                storyMessage: storyMessage,
                tx: transaction
            )
            return buildQuotedReplyModel(originalContent: .textStory({ spoilerState in
                return TextAttachmentView(
                    attachment: preloadedTextAttachment,
                    interactionIdentifier: .fromStoryMessage(storyMessage),
                    spoilerState: spoilerState
                ).asThumbnailView()
            }))
        }
     }

    public static func build(
        storyReplyMessage message: TSMessage,
        storyTimestamp: UInt64,
        storyAuthorAci: Aci,
        transaction: SDSAnyReadTransaction
    ) -> QuotedReplyModel {
        guard let storyMessage = StoryFinder.story(
            timestamp: storyTimestamp,
            author: storyAuthorAci,
            transaction: transaction
        ) else {
            let isOriginalMessageAuthorLocalUser = DependenciesBridge.shared.tsAccountManager
                .localIdentifiers(tx: transaction.asV2Read)?
                .aci == storyAuthorAci
            return QuotedReplyModel(
                originalMessageTimestamp: storyTimestamp,
                originalMessageAuthorAddress: SignalServiceAddress(storyAuthorAci),
                isOriginalMessageAuthorLocalUser: isOriginalMessageAuthorLocalUser,
                storyReactionEmoji: message.storyReactionEmoji,
                originalContent: .expiredStory,
                sourceOfOriginal: .story
            )
        }
        return QuotedReplyModel.build(
            replyingTo: storyMessage,
            reactionEmoji: message.storyReactionEmoji,
            transaction: transaction
        )
    }

    // Used for persisted quoted replies, both incoming and outgoing.
    public static func build(
        replyMessage message: TSMessage,
        quotedMessage: TSQuotedMessage,
        transaction: SDSAnyReadTransaction
    ) -> QuotedReplyModel {
        func buildQuotedReplyModel(
            originalContent: OriginalContent
        ) -> QuotedReplyModel {
            return QuotedReplyModel(
                originalMessageTimestamp: quotedMessage.timestampValue?.uint64Value,
                originalMessageAuthorAddress: quotedMessage.authorAddress,
                isOriginalMessageAuthorLocalUser: message.isOutgoing,
                storyReactionEmoji: nil,
                originalContent: originalContent,
                sourceOfOriginal: quotedMessage.bodySource
            )
        }

        let originalMessageBody: MessageBody? = quotedMessage.body.map {
            MessageBody(text: $0, ranges: quotedMessage.bodyRanges ?? .empty)
        }

        if quotedMessage.isGiftBadge {
            return buildQuotedReplyModel(originalContent: .giftBadge)
        }

        let attachmentReference = DependenciesBridge.shared.tsResourceStore.quotedAttachmentReference(
            for: message,
            tx: transaction.asV2Read
        )

        switch attachmentReference {
        case nil:
            break
        case .stub(let stub):
            return buildQuotedReplyModel(originalContent: .attachmentStub(originalMessageBody, stub))
        case .thumbnail(let attachmentRef):
            // Fetch the full attachment.
            let thumbnailAttachment = DependenciesBridge.shared.tsResourceStore.fetch(
                attachmentRef.resourceId,
                tx: transaction.asV2Read
            )
            let image: UIImage? = {
                if
                    let thumbnailAttachment,
                    let image = DependenciesBridge.shared.tsResourceManager.thumbnailImage(
                        attachment: thumbnailAttachment,
                        parentMessage: message,
                        tx: transaction.asV2Read
                    )
                {
                    return image
                } else if
                    let blurHash = thumbnailAttachment?.resourceBlurHash,
                    let image = BlurHash.image(for: blurHash)
                {
                    return image
                } else {
                    return nil
                }
            }()
            if let thumbnailAttachment {
                return buildQuotedReplyModel(originalContent: .attachment(
                    originalMessageBody,
                    attachment: .init(reference: attachmentRef, attachment: thumbnailAttachment),
                    thumbnailImage: image
                ))
            } else {
                break
            }
        }

        return buildQuotedReplyModel(originalContent: .text(originalMessageBody))
    }

    private init(
        originalMessageTimestamp: UInt64?,
        originalMessageAuthorAddress: SignalServiceAddress,
        isOriginalMessageAuthorLocalUser: Bool,
        storyReactionEmoji: String?,
        originalContent: OriginalContent,
        sourceOfOriginal: TSQuotedMessageContentSource
    ) {
        self.originalMessageTimestamp = originalMessageTimestamp
        self.originalMessageAuthorAddress = originalMessageAuthorAddress
        self.isOriginalMessageAuthorLocalUser = isOriginalMessageAuthorLocalUser
        self.storyReactionEmoji = storyReactionEmoji
        self.originalContent = originalContent
        self.sourceOfOriginal = sourceOfOriginal
    }
}

// MARK: - Equatable

extension QuotedReplyModel: Equatable {
    public static func == (lhs: QuotedReplyModel, rhs: QuotedReplyModel) -> Bool {
        return lhs.originalMessageTimestamp == rhs.originalMessageTimestamp
            && lhs.originalMessageAuthorAddress == rhs.originalMessageAuthorAddress
            && lhs.isOriginalMessageAuthorLocalUser == rhs.isOriginalMessageAuthorLocalUser
            && lhs.storyReactionEmoji == rhs.storyReactionEmoji
            && lhs.originalContent == rhs.originalContent
            && lhs.sourceOfOriginal == rhs.sourceOfOriginal
    }
}

extension QuotedReplyModel.OriginalContent: Equatable {
    public static func == (lhs: QuotedReplyModel.OriginalContent, rhs: QuotedReplyModel.OriginalContent) -> Bool {
        switch (lhs, rhs) {
        case let (.text(lhsBody), .text(rhsBody)):
            return lhsBody == rhsBody
        case (.giftBadge, .giftBadge):
            return true
        case let (.storyReactionEmoji(lhsString), .storyReactionEmoji(rhsString)):
            return lhsString == rhsString
        case let (.attachmentStub(lhsBody, lhsStub), .attachmentStub(rhsBody, rhsStub)):
            return lhsBody == rhsBody && lhsStub == rhsStub
        case let (.attachment(lhsBody, lhsAttachment, lhsImage), .attachment(rhsBody, rhsAttachment, rhsImage)):
            return lhsBody == rhsBody
                && lhsAttachment.attachment.resourceId == rhsAttachment.attachment.resourceId
                && lhsImage == rhsImage
        case let (.mediaStory(lhsBody, lhsAttachment, lhsImage), .mediaStory(rhsBody, rhsAttachment, rhsImage)):
            return lhsBody == rhsBody
                && lhsAttachment.attachment.resourceId == rhsAttachment.attachment.resourceId
                && lhsImage == rhsImage
        case (.textStory(_), .textStory(_)):
            /// Defensively re-render every time.
            return false
        case (.expiredStory, .expiredStory):
            return true

        case
            (.text, _),
            (.giftBadge, _),
            (.storyReactionEmoji, _),
            (.attachmentStub, _),
            (.attachment, _),
            (.mediaStory, _),
            (.textStory, _),
            (.expiredStory, _):
            return false
        }
    }

}
