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

#import "TSMessage.h"
#import "TSQuotedMessage.h"
#import <SignalServiceKit/SignalServiceKit-Swift.h>

NS_ASSUME_NONNULL_BEGIN

static const NSUInteger OWSMessageSchemaVersion = 4;

#pragma mark -

@interface TSMessage ()

@property (nonatomic, nullable) NSString *body;
@property (nonatomic, nullable) MessageBodyRanges *bodyRanges;

@property (nonatomic) uint32_t expiresInSeconds;
@property (nonatomic) uint64_t expireStartedAt;
@property (nonatomic, nullable) NSNumber *expireTimerVersion;

/**
 * The version of the model class's schema last used to serialize this model. Use this to manage data migrations during
 * object de/serialization.
 *
 * e.g.
 *
 *    - (id)initWithCoder:(NSCoder *)coder
 *    {
 *      self = [super initWithCoder:coder];
 *      if (!self) { return self; }
 *      if (_schemaVersion < 2) {
 *        _newName = [coder decodeObjectForKey:@"oldName"]
 *      }
 *      ...
 *      _schemaVersion = 2;
 *    }
 */
@property (nonatomic, readonly) NSUInteger schemaVersion;

@property (nonatomic, nullable) TSQuotedMessage *quotedMessage;
@property (nonatomic, nullable) OWSContact *contactShare;
@property (nonatomic, nullable) OWSLinkPreview *linkPreview;
@property (nonatomic, nullable) MessageSticker *messageSticker;

@property (nonatomic) BOOL isViewOnceMessage;
@property (nonatomic) BOOL isViewOnceComplete;
@property (nonatomic) BOOL wasRemotelyDeleted;

@property (nonatomic, nullable) NSString *storyReactionEmoji;

@property (nonatomic) BOOL isPoll;

@end

#pragma mark -

@implementation TSMessage

- (instancetype)initMessageWithBuilder:(TSMessageBuilder *)messageBuilder
{
    self = [super initWithTimestamp:messageBuilder.timestamp
                receivedAtTimestamp:messageBuilder.receivedAtTimestamp
                             thread:messageBuilder.thread];
    if (!self) {
        return self;
    }

    _schemaVersion = OWSMessageSchemaVersion;

    if (messageBuilder.messageBody.length > 0) {
        _body = messageBuilder.messageBody;
        _bodyRanges = messageBuilder.bodyRanges;
    }
    _deprecated_attachmentIds = nil;
    _editState = messageBuilder.editState;
    _expiresInSeconds = messageBuilder.expiresInSeconds;
    _expireStartedAt = messageBuilder.expireStartedAt;
    _expireTimerVersion = messageBuilder.expireTimerVersion;
    [self updateExpiresAt];
    _isSmsMessageRestoredFromBackup = messageBuilder.isSmsMessageRestoredFromBackup;
    _isViewOnceMessage = messageBuilder.isViewOnceMessage;
    _isViewOnceComplete = messageBuilder.isViewOnceComplete;
    _wasRemotelyDeleted = messageBuilder.wasRemotelyDeleted;

    _storyTimestamp = messageBuilder.storyTimestamp;
    _storyAuthorUuidString = messageBuilder.storyAuthorAci.serviceIdUppercaseString;
    _storyReactionEmoji = messageBuilder.storyReactionEmoji;
    _isGroupStoryReply = messageBuilder.isGroupStoryReply;

    _quotedMessage = messageBuilder.quotedMessage;
    _contactShare = messageBuilder.contactShare;
    _linkPreview = messageBuilder.linkPreview;
    _messageSticker = messageBuilder.messageSticker;
    _giftBadge = messageBuilder.giftBadge;
    _isPoll = messageBuilder.isPoll;

    return self;
}

// --- CODE GENERATION MARKER

// This snippet is generated by /Scripts/sds_codegen/sds_generate.py. Do not manually edit it, instead run
// `sds_codegen.sh`.

// clang-format off

- (instancetype)initWithGrdbId:(int64_t)grdbId
                      uniqueId:(NSString *)uniqueId
             receivedAtTimestamp:(uint64_t)receivedAtTimestamp
                          sortId:(uint64_t)sortId
                       timestamp:(uint64_t)timestamp
                  uniqueThreadId:(NSString *)uniqueThreadId
                            body:(nullable NSString *)body
                      bodyRanges:(nullable MessageBodyRanges *)bodyRanges
                    contactShare:(nullable OWSContact *)contactShare
        deprecated_attachmentIds:(nullable NSArray<NSString *> *)deprecated_attachmentIds
                       editState:(TSEditState)editState
                 expireStartedAt:(uint64_t)expireStartedAt
              expireTimerVersion:(nullable NSNumber *)expireTimerVersion
                       expiresAt:(uint64_t)expiresAt
                expiresInSeconds:(unsigned int)expiresInSeconds
                       giftBadge:(nullable OWSGiftBadge *)giftBadge
               isGroupStoryReply:(BOOL)isGroupStoryReply
                          isPoll:(BOOL)isPoll
  isSmsMessageRestoredFromBackup:(BOOL)isSmsMessageRestoredFromBackup
              isViewOnceComplete:(BOOL)isViewOnceComplete
               isViewOnceMessage:(BOOL)isViewOnceMessage
                     linkPreview:(nullable OWSLinkPreview *)linkPreview
                  messageSticker:(nullable MessageSticker *)messageSticker
                   quotedMessage:(nullable TSQuotedMessage *)quotedMessage
    storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer
           storyAuthorUuidString:(nullable NSString *)storyAuthorUuidString
              storyReactionEmoji:(nullable NSString *)storyReactionEmoji
                  storyTimestamp:(nullable NSNumber *)storyTimestamp
              wasRemotelyDeleted:(BOOL)wasRemotelyDeleted
{
    self = [super initWithGrdbId:grdbId
                        uniqueId:uniqueId
               receivedAtTimestamp:receivedAtTimestamp
                            sortId:sortId
                         timestamp:timestamp
                    uniqueThreadId:uniqueThreadId];

    if (!self) {
        return self;
    }

    _body = body;
    _bodyRanges = bodyRanges;
    _contactShare = contactShare;
    _deprecated_attachmentIds = deprecated_attachmentIds;
    _editState = editState;
    _expireStartedAt = expireStartedAt;
    _expireTimerVersion = expireTimerVersion;
    _expiresAt = expiresAt;
    _expiresInSeconds = expiresInSeconds;
    _giftBadge = giftBadge;
    _isGroupStoryReply = isGroupStoryReply;
    _isPoll = isPoll;
    _isSmsMessageRestoredFromBackup = isSmsMessageRestoredFromBackup;
    _isViewOnceComplete = isViewOnceComplete;
    _isViewOnceMessage = isViewOnceMessage;
    _linkPreview = linkPreview;
    _messageSticker = messageSticker;
    _quotedMessage = quotedMessage;
    _storedShouldStartExpireTimer = storedShouldStartExpireTimer;
    _storyAuthorUuidString = storyAuthorUuidString;
    _storyReactionEmoji = storyReactionEmoji;
    _storyTimestamp = storyTimestamp;
    _wasRemotelyDeleted = wasRemotelyDeleted;

    [self sdsFinalizeMessage];

    return self;
}

// clang-format on

// --- CODE GENERATION MARKER

- (void)sdsFinalizeMessage
{
    [self updateExpiresAt];
}

- (void)encodeWithCoder:(NSCoder *)coder
{
    [super encodeWithCoder:coder];
    NSString *body = self.body;
    if (body != nil) {
        [coder encodeObject:body forKey:@"body"];
    }
    MessageBodyRanges *bodyRanges = self.bodyRanges;
    if (bodyRanges != nil) {
        [coder encodeObject:bodyRanges forKey:@"bodyRanges"];
    }
    OWSContact *contactShare = self.contactShare;
    if (contactShare != nil) {
        [coder encodeObject:contactShare forKey:@"contactShare"];
    }
    NSArray *deprecated_attachmentIds = self.deprecated_attachmentIds;
    if (deprecated_attachmentIds != nil) {
        [coder encodeObject:deprecated_attachmentIds forKey:@"deprecated_attachmentIds"];
    }
    [coder encodeObject:[self valueForKey:@"editState"] forKey:@"editState"];
    [coder encodeObject:[self valueForKey:@"expireStartedAt"] forKey:@"expireStartedAt"];
    NSNumber *expireTimerVersion = self.expireTimerVersion;
    if (expireTimerVersion != nil) {
        [coder encodeObject:expireTimerVersion forKey:@"expireTimerVersion"];
    }
    [coder encodeObject:[self valueForKey:@"expiresAt"] forKey:@"expiresAt"];
    [coder encodeObject:[self valueForKey:@"expiresInSeconds"] forKey:@"expiresInSeconds"];
    OWSGiftBadge *giftBadge = self.giftBadge;
    if (giftBadge != nil) {
        [coder encodeObject:giftBadge forKey:@"giftBadge"];
    }
    [coder encodeObject:[self valueForKey:@"isGroupStoryReply"] forKey:@"isGroupStoryReply"];
    [coder encodeObject:[self valueForKey:@"isPoll"] forKey:@"isPoll"];
    [coder encodeObject:[self valueForKey:@"isSmsMessageRestoredFromBackup"] forKey:@"isSmsMessageRestoredFromBackup"];
    [coder encodeObject:[self valueForKey:@"isViewOnceComplete"] forKey:@"isViewOnceComplete"];
    [coder encodeObject:[self valueForKey:@"isViewOnceMessage"] forKey:@"isViewOnceMessage"];
    OWSLinkPreview *linkPreview = self.linkPreview;
    if (linkPreview != nil) {
        [coder encodeObject:linkPreview forKey:@"linkPreview"];
    }
    MessageSticker *messageSticker = self.messageSticker;
    if (messageSticker != nil) {
        [coder encodeObject:messageSticker forKey:@"messageSticker"];
    }
    TSQuotedMessage *quotedMessage = self.quotedMessage;
    if (quotedMessage != nil) {
        [coder encodeObject:quotedMessage forKey:@"quotedMessage"];
    }
    [coder encodeObject:[self valueForKey:@"schemaVersion"] forKey:@"schemaVersion"];
    [coder encodeObject:[self valueForKey:@"storedShouldStartExpireTimer"] forKey:@"storedShouldStartExpireTimer"];
    NSString *storyAuthorUuidString = self.storyAuthorUuidString;
    if (storyAuthorUuidString != nil) {
        [coder encodeObject:storyAuthorUuidString forKey:@"storyAuthorUuidString"];
    }
    NSString *storyReactionEmoji = self.storyReactionEmoji;
    if (storyReactionEmoji != nil) {
        [coder encodeObject:storyReactionEmoji forKey:@"storyReactionEmoji"];
    }
    NSNumber *storyTimestamp = self.storyTimestamp;
    if (storyTimestamp != nil) {
        [coder encodeObject:storyTimestamp forKey:@"storyTimestamp"];
    }
    [coder encodeObject:[self valueForKey:@"wasRemotelyDeleted"] forKey:@"wasRemotelyDeleted"];
}

- (nullable instancetype)initWithCoder:(NSCoder *)coder
{
    self = [super initWithCoder:coder];
    if (!self) {
        return self;
    }
    self->_body = [coder decodeObjectOfClass:[NSString class] forKey:@"body"];
    self->_bodyRanges = [coder decodeObjectOfClass:[MessageBodyRanges class] forKey:@"bodyRanges"];
    self->_contactShare = [coder decodeObjectOfClass:[OWSContact class] forKey:@"contactShare"];
    self->_deprecated_attachmentIds =
        [coder decodeObjectOfClasses:[NSSet setWithArray:@[ [NSArray class], [NSString class] ]]
                              forKey:@"deprecated_attachmentIds"];
    self->_editState = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class] forKey:@"editState"] integerValue];
    self->_expireStartedAt = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
                                                              forKey:@"expireStartedAt"] unsignedLongLongValue];
    self->_expireTimerVersion = [coder decodeObjectOfClass:[NSNumber class] forKey:@"expireTimerVersion"];
    self->_expiresAt = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
                                                        forKey:@"expiresAt"] unsignedLongLongValue];
    self->_expiresInSeconds = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
                                                               forKey:@"expiresInSeconds"] unsignedIntValue];
    self->_giftBadge = [coder decodeObjectOfClass:[OWSGiftBadge class] forKey:@"giftBadge"];
    self->_isGroupStoryReply = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
                                                                forKey:@"isGroupStoryReply"] boolValue];
    self->_isPoll = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class] forKey:@"isPoll"] boolValue];
    self->_isSmsMessageRestoredFromBackup =
        [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class] forKey:@"isSmsMessageRestoredFromBackup"] boolValue];
    self->_isViewOnceComplete = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
                                                                 forKey:@"isViewOnceComplete"] boolValue];
    self->_isViewOnceMessage = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
                                                                forKey:@"isViewOnceMessage"] boolValue];
    self->_linkPreview = [coder decodeObjectOfClass:[OWSLinkPreview class] forKey:@"linkPreview"];
    self->_messageSticker = [coder decodeObjectOfClass:[MessageSticker class] forKey:@"messageSticker"];
    self->_quotedMessage = [coder decodeObjectOfClass:[TSQuotedMessage class] forKey:@"quotedMessage"];
    self->_schemaVersion = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
                                                            forKey:@"schemaVersion"] unsignedIntegerValue];
    self->_storedShouldStartExpireTimer =
        [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class] forKey:@"storedShouldStartExpireTimer"] boolValue];
    self->_storyAuthorUuidString = [coder decodeObjectOfClass:[NSString class] forKey:@"storyAuthorUuidString"];
    self->_storyReactionEmoji = [coder decodeObjectOfClass:[NSString class] forKey:@"storyReactionEmoji"];
    self->_storyTimestamp = [coder decodeObjectOfClass:[NSNumber class] forKey:@"storyTimestamp"];
    self->_wasRemotelyDeleted = [(NSNumber *)[coder decodeObjectOfClass:[NSNumber class]
                                                                 forKey:@"wasRemotelyDeleted"] boolValue];

    if (_schemaVersion < 2) {
        // renamed _attachments to _attachmentIds
        if (!_deprecated_attachmentIds) {
            _deprecated_attachmentIds =
                [coder decodeObjectOfClasses:[NSSet setWithArray:@[ [NSArray class], [NSString class] ]]
                                      forKey:@"attachments"];
        }
    }

    if (_schemaVersion < 3) {
        _expiresInSeconds = 0;
        _expireStartedAt = 0;
        _expiresAt = 0;
    }

    if (_schemaVersion < 4) {
        // Wipe out the body field on these legacy attachment messages.
        //
        // Explanation: Historically, a message sent from iOS could be an attachment XOR a text message,
        // but now we support sending an attachment+caption as a single message.
        //
        // Other clients have supported sending attachment+caption in a single message for a long time.
        // So the way we used to handle receiving them was to make it look like they'd sent two messages:
        // first the attachment+caption (we'd ignore this caption when rendering), followed by a separate
        // message with just the caption (which we'd render as a simple independent text message), for
        // which we'd offset the timestamp by a little bit to get the desired ordering.
        //
        // Now that we can properly render an attachment+caption message together, these legacy "dummy" text
        // messages are not only unnecessary, but worse, would be rendered redundantly. For safety, rather
        // than building the logic to try to find and delete the redundant "dummy" text messages which users
        // have been seeing and interacting with, we delete the body field from the attachment message,
        // which iOS users have never seen directly.
        if (_deprecated_attachmentIds.count > 0) {
            _body = nil;
        }
    }

    _schemaVersion = OWSMessageSchemaVersion;

    // Upgrades legacy messages.
    //
    // TODO: We can eventually remove this migration since
    //       per-message expiration was never released to
    //       production.
    NSNumber *_Nullable perMessageExpirationDurationSeconds =
        [coder decodeObjectOfClass:[NSNumber class] forKey:@"perMessageExpirationDurationSeconds"];
    if (perMessageExpirationDurationSeconds.unsignedIntegerValue > 0) {
        _isViewOnceMessage = YES;
    }
    NSNumber *_Nullable perMessageExpirationHasExpired = [coder decodeObjectOfClass:[NSNumber class]
                                                                             forKey:@"perMessageExpirationHasExpired"];
    if (perMessageExpirationHasExpired.boolValue > 0) {
        _isViewOnceComplete = YES;
    }

    return self;
}

- (NSUInteger)hash
{
    NSUInteger result = [super hash];
    result ^= self.body.hash;
    result ^= self.bodyRanges.hash;
    result ^= self.contactShare.hash;
    result ^= self.deprecated_attachmentIds.hash;
    result ^= (NSUInteger)self.editState;
    result ^= self.expireStartedAt;
    result ^= self.expireTimerVersion.hash;
    result ^= self.expiresAt;
    result ^= self.expiresInSeconds;
    result ^= self.giftBadge.hash;
    result ^= self.isGroupStoryReply;
    result ^= self.isPoll;
    result ^= self.isSmsMessageRestoredFromBackup;
    result ^= self.isViewOnceComplete;
    result ^= self.isViewOnceMessage;
    result ^= self.linkPreview.hash;
    result ^= self.messageSticker.hash;
    result ^= self.quotedMessage.hash;
    result ^= self.schemaVersion;
    result ^= self.storedShouldStartExpireTimer;
    result ^= self.storyAuthorUuidString.hash;
    result ^= self.storyReactionEmoji.hash;
    result ^= self.storyTimestamp.hash;
    result ^= self.wasRemotelyDeleted;
    return result;
}

- (BOOL)isEqual:(id)other
{
    if (![super isEqual:other]) {
        return NO;
    }
    TSMessage *typedOther = (TSMessage *)other;
    if (![NSObject isObject:self.body equalToObject:typedOther.body]) {
        return NO;
    }
    if (![NSObject isObject:self.bodyRanges equalToObject:typedOther.bodyRanges]) {
        return NO;
    }
    if (![NSObject isObject:self.contactShare equalToObject:typedOther.contactShare]) {
        return NO;
    }
    if (![NSObject isObject:self.deprecated_attachmentIds equalToObject:typedOther.deprecated_attachmentIds]) {
        return NO;
    }
    if (self.editState != typedOther.editState) {
        return NO;
    }
    if (self.expireStartedAt != typedOther.expireStartedAt) {
        return NO;
    }
    if (![NSObject isObject:self.expireTimerVersion equalToObject:typedOther.expireTimerVersion]) {
        return NO;
    }
    if (self.expiresAt != typedOther.expiresAt) {
        return NO;
    }
    if (self.expiresInSeconds != typedOther.expiresInSeconds) {
        return NO;
    }
    if (![NSObject isObject:self.giftBadge equalToObject:typedOther.giftBadge]) {
        return NO;
    }
    if (self.isGroupStoryReply != typedOther.isGroupStoryReply) {
        return NO;
    }
    if (self.isPoll != typedOther.isPoll) {
        return NO;
    }
    if (self.isSmsMessageRestoredFromBackup != typedOther.isSmsMessageRestoredFromBackup) {
        return NO;
    }
    if (self.isViewOnceComplete != typedOther.isViewOnceComplete) {
        return NO;
    }
    if (self.isViewOnceMessage != typedOther.isViewOnceMessage) {
        return NO;
    }
    if (![NSObject isObject:self.linkPreview equalToObject:typedOther.linkPreview]) {
        return NO;
    }
    if (![NSObject isObject:self.messageSticker equalToObject:typedOther.messageSticker]) {
        return NO;
    }
    if (![NSObject isObject:self.quotedMessage equalToObject:typedOther.quotedMessage]) {
        return NO;
    }
    if (self.schemaVersion != typedOther.schemaVersion) {
        return NO;
    }
    if (self.storedShouldStartExpireTimer != typedOther.storedShouldStartExpireTimer) {
        return NO;
    }
    if (![NSObject isObject:self.storyAuthorUuidString equalToObject:typedOther.storyAuthorUuidString]) {
        return NO;
    }
    if (![NSObject isObject:self.storyReactionEmoji equalToObject:typedOther.storyReactionEmoji]) {
        return NO;
    }
    if (![NSObject isObject:self.storyTimestamp equalToObject:typedOther.storyTimestamp]) {
        return NO;
    }
    if (self.wasRemotelyDeleted != typedOther.wasRemotelyDeleted) {
        return NO;
    }
    return YES;
}

- (void)setExpireStartedAt:(uint64_t)expireStartedAt
{
    if (_expireStartedAt != 0 && _expireStartedAt < expireStartedAt) {
        return;
    }

    uint64_t now = [NSDate ows_millisecondTimeStamp];
    if (expireStartedAt > now) {
        OWSLogWarn(@"using `now` instead of future time");
    }

    _expireStartedAt = MIN(now, expireStartedAt);

    [self updateExpiresAt];
}

// This method will be called after every insert and update, so it needs
// to be cheap.
- (BOOL)shouldStartExpireTimer
{
    if (self.hasPerConversationExpirationStarted) {
        // Expiration already started.
        return YES;
    }

    return self.hasPerConversationExpiration;
}

- (void)updateExpiresAt
{
    if (self.hasPerConversationExpirationStarted) {
        _expiresAt = _expireStartedAt + (uint64_t)_expiresInSeconds * 1000;
    } else {
        _expiresAt = 0;
    }
}

#pragma mark - Story Context

- (nullable AciObjC *)storyAuthorAci
{
    return [[AciObjC alloc] initWithAciString:self.storyAuthorUuidString];
}

- (BOOL)isStoryReply
{
    return self.storyAuthorUuidString != nil;
}

- (NSString *)debugDescription
{
    return [NSString stringWithFormat:@"%@ with body: %@ has mentions: %@",
        [self class],
        self.body,
        self.bodyRanges.hasMentions ? @"YES" : @"NO"];
}


- (void)anyWillInsertWithTransaction:(DBWriteTransaction *)transaction
{
    [super anyWillInsertWithTransaction:transaction];

    [self insertMentionsInDatabaseWithTx:transaction];
}

- (void)anyDidInsertWithTransaction:(DBWriteTransaction *)transaction
{
    [super anyDidInsertWithTransaction:transaction];

    [self _anyDidInsertWithTx:transaction];

    [self ensurePerConversationExpirationWithTransaction:transaction];

    [self touchStoryMessageIfNecessaryWithReplyCountIncrement:ReplyCountIncrementNewReplyAdded transaction:transaction];
}

- (void)anyWillUpdateWithTransaction:(DBWriteTransaction *)transaction
{
    [super anyWillUpdateWithTransaction:transaction];
}

- (void)anyDidUpdateWithTransaction:(DBWriteTransaction *)transaction
{
    [super anyDidUpdateWithTransaction:transaction];

    [self _anyDidUpdateWithTx:transaction];

    [self ensurePerConversationExpirationWithTransaction:transaction];

    [self touchStoryMessageIfNecessaryWithReplyCountIncrement:ReplyCountIncrementNoIncrement transaction:transaction];
}

- (void)ensurePerConversationExpirationWithTransaction:(DBWriteTransaction *)transaction
{
    if (self.hasPerConversationExpirationStarted) {
        // Expiration already started.
        return;
    }
    if (![self shouldStartExpireTimer]) {
        return;
    }
    uint64_t nowMs = [NSDate ows_millisecondTimeStamp];
    [DisappearingMessagesExpirationJobObjcBridge startExpirationForMessage:self
                                                       expirationStartedAt:nowMs
                                                                        tx:transaction];
}

- (BOOL)hasPerConversationExpiration
{
    return self.expiresInSeconds > 0;
}

- (BOOL)hasPerConversationExpirationStarted
{
    return _expireStartedAt > 0 && _expiresInSeconds > 0;
}

- (BOOL)shouldUseReceiptDateForSorting
{
    return YES;
}

- (nullable NSString *)body
{
    if (self.isPoll) {
        return _body;
    }

    return _body.filterStringForDisplay;
}

#pragma mark - Update With... Methods

- (void)updateWithExpireStartedAt:(uint64_t)expireStartedAt transaction:(DBWriteTransaction *)transaction
{
    OWSAssertDebug(expireStartedAt > 0);
    OWSAssertDebug(self.expiresInSeconds > 0);

    [self anyUpdateMessageWithTransaction:transaction
                                    block:^(TSMessage *message) { [message setExpireStartedAt:expireStartedAt]; }];
}

- (void)updateWithLinkPreview:(OWSLinkPreview *)linkPreview transaction:(DBWriteTransaction *)transaction
{
    OWSAssertDebug(linkPreview);
    OWSAssertDebug(transaction);

    [self anyUpdateMessageWithTransaction:transaction
                                    block:^(TSMessage *message) { [message setLinkPreview:linkPreview]; }];
}

- (void)updateWithQuotedMessage:(TSQuotedMessage *)quotedMessage transaction:(DBWriteTransaction *)transaction
{
    OWSAssertDebug(quotedMessage);
    OWSAssertDebug(transaction);

    [self anyUpdateMessageWithTransaction:transaction
                                    block:^(TSMessage *message) { [message setQuotedMessage:quotedMessage]; }];
}

- (void)updateWithMessageSticker:(MessageSticker *)messageSticker transaction:(DBWriteTransaction *)transaction
{
    OWSAssertDebug(messageSticker);
    OWSAssertDebug(transaction);

    [self anyUpdateMessageWithTransaction:transaction
                                    block:^(TSMessage *message) { message.messageSticker = messageSticker; }];
}

- (void)updateWithContactShare:(OWSContact *)contactShare transaction:(DBWriteTransaction *)transaction
{
    OWSAssertDebug(contactShare);
    OWSAssertDebug(transaction);

    [self anyUpdateMessageWithTransaction:transaction
                                    block:^(TSMessage *message) { message.contactShare = contactShare; }];
}

- (void)updateWithIsPoll:(BOOL)isPoll transaction:(DBWriteTransaction *)transaction
{
    OWSAssertDebug(transaction);

    [self anyUpdateMessageWithTransaction:transaction block:^(TSMessage *message) { message.isPoll = isPoll; }];
}

#ifdef TESTABLE_BUILD

// This method is for testing purposes only.
- (void)updateWithMessageBody:(nullable NSString *)messageBody transaction:(DBWriteTransaction *)transaction
{
    OWSAssertDebug(transaction);

    [self anyUpdateMessageWithTransaction:transaction block:^(TSMessage *message) { message.body = messageBody; }];
}

#endif

#pragma mark - View Once

- (void)updateWithViewOnceCompleteAndRemoveRenderableContentWithTransaction:(DBWriteTransaction *)transaction
{
    OWSAssertDebug(transaction);
    OWSAssertDebug(self.isViewOnceMessage);
    OWSAssertDebug(!self.isViewOnceComplete);

    [self removeAllRenderableContentWithTransaction:transaction
                                 messageUpdateBlock:^(TSMessage *message) { message.isViewOnceComplete = YES; }];
}

#pragma mark - Remote Delete

- (void)updateWithRemotelyDeletedAndRemoveRenderableContentWithTransaction:(DBWriteTransaction *)transaction
{
    OWSAssertDebug(transaction);
    OWSAssertDebug(!self.wasRemotelyDeleted);

    [self removeAllReactionsWithTransaction:transaction];

    [self unpinMessageIfNeededWithTx:transaction];

    [self removeAllRenderableContentWithTransaction:transaction
                                 messageUpdateBlock:^(TSMessage *message) { message.wasRemotelyDeleted = YES; }];
}

#pragma mark - Remove Renderable Content

- (void)removeAllRenderableContentWithTransaction:(DBWriteTransaction *)transaction
                               messageUpdateBlock:(void (^)(TSMessage *message))messageUpdateBlock
{
    // We call removeAllAttachmentsWithTransaction() before
    // anyUpdateWithTransaction, because anyUpdateWithTransaction's
    // block can be called twice, once on this instance and once
    // on the copy from the database.  We only want to remove
    // attachments once.
    [self removeAllAttachmentsWithTx:transaction];
    [self removeAllMentionsWithTransaction:transaction];
    [MessageSendLogObjC deleteAllPayloadsForInteraction:self tx:transaction];

    [self anyUpdateMessageWithTransaction:transaction
                                    block:^(TSMessage *message) {
                                        // Remove renderable content.
                                        message.body = nil;
                                        message.bodyRanges = nil;
                                        message.contactShare = nil;
                                        message.quotedMessage = nil;
                                        message.linkPreview = nil;
                                        message.messageSticker = nil;
                                        message.storyReactionEmoji = nil;

                                        messageUpdateBlock(message);
                                    }];
}

@end

NS_ASSUME_NONNULL_END
