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

#import "TSThread.h"
#import "AppReadiness.h"
#import "OWSDisappearingMessagesConfiguration.h"
#import "OWSReadTracking.h"
#import "TSIncomingMessage.h"
#import "TSInfoMessage.h"
#import "TSInteraction.h"
#import "TSInvalidIdentityKeyReceivingErrorMessage.h"
#import "TSOutgoingMessage.h"
#import <SignalCoreKit/Cryptography.h>
#import <SignalCoreKit/NSDate+OWS.h>
#import <SignalCoreKit/NSString+OWS.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>

@import Intents;

NS_ASSUME_NONNULL_BEGIN

@interface TSThread ()

@property (nonatomic) TSThreadStoryViewMode storyViewMode;
@property (nonatomic, nullable) NSNumber *lastSentStoryTimestamp;

@property (nonatomic, nullable) NSDate *creationDate;
@property (nonatomic) BOOL isArchivedObsolete;
@property (nonatomic) BOOL isMarkedUnreadObsolete;

@property (atomic) uint64_t mutedUntilTimestampObsolete;

@property (nonatomic, nullable) NSDate *mutedUntilDateObsolete;
@property (nonatomic) uint64_t lastVisibleSortIdObsolete;
@property (nonatomic) double lastVisibleSortIdOnScreenPercentageObsolete;

@property (nonatomic) TSThreadMentionNotificationMode mentionNotificationMode;

@end

#pragma mark -

@implementation TSThread

+ (NSString *)collection
{
    return @"TSThread";
}

- (instancetype)initWithUniqueId:(NSString *)uniqueId
{
    self = [super initWithUniqueId:uniqueId];

    if (self) {
        _creationDate = [NSDate date];
        _messageDraft = nil;
        _conversationColorNameObsolete = @"Obsolete";
    }

    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
   conversationColorNameObsolete:(NSString *)conversationColorNameObsolete
                    creationDate:(nullable NSDate *)creationDate
             editTargetTimestamp:(nullable NSNumber *)editTargetTimestamp
              isArchivedObsolete:(BOOL)isArchivedObsolete
          isMarkedUnreadObsolete:(BOOL)isMarkedUnreadObsolete
            lastInteractionRowId:(uint64_t)lastInteractionRowId
          lastSentStoryTimestamp:(nullable NSNumber *)lastSentStoryTimestamp
       lastVisibleSortIdObsolete:(uint64_t)lastVisibleSortIdObsolete
lastVisibleSortIdOnScreenPercentageObsolete:(double)lastVisibleSortIdOnScreenPercentageObsolete
         mentionNotificationMode:(TSThreadMentionNotificationMode)mentionNotificationMode
                    messageDraft:(nullable NSString *)messageDraft
          messageDraftBodyRanges:(nullable MessageBodyRanges *)messageDraftBodyRanges
          mutedUntilDateObsolete:(nullable NSDate *)mutedUntilDateObsolete
     mutedUntilTimestampObsolete:(uint64_t)mutedUntilTimestampObsolete
           shouldThreadBeVisible:(BOOL)shouldThreadBeVisible
                   storyViewMode:(TSThreadStoryViewMode)storyViewMode
{
    self = [super initWithGrdbId:grdbId
                        uniqueId:uniqueId];

    if (!self) {
        return self;
    }

    _conversationColorNameObsolete = conversationColorNameObsolete;
    _creationDate = creationDate;
    _editTargetTimestamp = editTargetTimestamp;
    _isArchivedObsolete = isArchivedObsolete;
    _isMarkedUnreadObsolete = isMarkedUnreadObsolete;
    _lastInteractionRowId = lastInteractionRowId;
    _lastSentStoryTimestamp = lastSentStoryTimestamp;
    _lastVisibleSortIdObsolete = lastVisibleSortIdObsolete;
    _lastVisibleSortIdOnScreenPercentageObsolete = lastVisibleSortIdOnScreenPercentageObsolete;
    _mentionNotificationMode = mentionNotificationMode;
    _messageDraft = messageDraft;
    _messageDraftBodyRanges = messageDraftBodyRanges;
    _mutedUntilDateObsolete = mutedUntilDateObsolete;
    _mutedUntilTimestampObsolete = mutedUntilTimestampObsolete;
    _shouldThreadBeVisible = shouldThreadBeVisible;
    _storyViewMode = storyViewMode;

    return self;
}

// clang-format on

// --- CODE GENERATION MARKER

- (nullable instancetype)initWithCoder:(NSCoder *)coder
{
    self = [super initWithCoder:coder];
    if (!self) {
        return self;
    }

    // renamed `hasEverHadMessage` -> `shouldThreadBeVisible`
    if (!_shouldThreadBeVisible) {
        NSNumber *_Nullable legacy_hasEverHadMessage = [coder decodeObjectForKey:@"hasEverHadMessage"];

        if (legacy_hasEverHadMessage != nil) {
            _shouldThreadBeVisible = legacy_hasEverHadMessage.boolValue;
        }
    }

    if (_conversationColorNameObsolete.length == 0) {
        _conversationColorNameObsolete = @"Obsolete";
    }

    NSDate *_Nullable lastMessageDate = [coder decodeObjectOfClass:NSDate.class forKey:@"lastMessageDate"];
    NSDate *_Nullable archivalDate = [coder decodeObjectOfClass:NSDate.class forKey:@"archivalDate"];
    _isArchivedByLegacyTimestampForSorting = [self.class legacyIsArchivedWithLastMessageDate:lastMessageDate
                                                                                archivalDate:archivalDate];

    if ([coder decodeObjectForKey:@"archivedAsOfMessageSortId"] != nil) {
        OWSAssertDebug(!_isArchivedObsolete);
        _isArchivedObsolete = YES;
    }

    return self;
}

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

    [ThreadAssociatedData createFor:self.uniqueId transaction:transaction];

    if (self.shouldThreadBeVisible && ![SSKPreferences hasSavedThreadWithTransaction:transaction]) {
        [SSKPreferences setHasSavedThread:YES transaction:transaction];
    }

    [self _anyDidInsertWithTx:transaction];

    [self.modelReadCaches.threadReadCache didInsertOrUpdateThread:self transaction:transaction];
}

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

    if (self.shouldThreadBeVisible && ![SSKPreferences hasSavedThreadWithTransaction:transaction]) {
        [SSKPreferences setHasSavedThread:YES transaction:transaction];
    }

    [self.modelReadCaches.threadReadCache didInsertOrUpdateThread:self transaction:transaction];

    [PinnedThreadManagerObjcBridge handleUpdatedThread:self transaction:transaction];
}

- (BOOL)isNoteToSelf
{
    return NO;
}

- (NSString *)colorSeed
{
    return self.uniqueId;
}

#pragma mark - To be subclassed.

- (NSArray<SignalServiceAddress *> *)recipientAddressesWithSneakyTransaction
{
    __block NSArray<SignalServiceAddress *> *recipientAddresses;
    [self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
        recipientAddresses = [self recipientAddressesWithTransaction:transaction];
    }];
    return recipientAddresses;
}


- (NSArray<SignalServiceAddress *> *)recipientAddressesWithTransaction:(SDSAnyReadTransaction *)transaction
{
    OWSAbstractMethod();

    return @[];
}

- (BOOL)hasSafetyNumbers
{
    return NO;
}

#pragma mark - Interactions

/**
 * Iterate over this thread's interactions.
 */
- (void)enumerateRecentInteractionsWithTransaction:(SDSAnyReadTransaction *)transaction
                                        usingBlock:(void (^)(TSInteraction *interaction))block
{
    NSError *error;
    InteractionFinder *interactionFinder = [[InteractionFinder alloc] initWithThreadUniqueId:self.uniqueId];
    [interactionFinder enumerateRecentInteractionsForConversationViewWithTransaction:transaction
                                                                               error:&error
                                                                               block:^BOOL(TSInteraction *interaction) {
                                                                                   block(interaction);
                                                                                   return YES;
                                                                               }];
    if (error != nil) {
        OWSFailDebug(@"Error during enumeration: %@", error);
    }
}

- (NSArray<TSInvalidIdentityKeyReceivingErrorMessage *> *)receivedMessagesForInvalidKey:(NSData *)key
                                                                                     tx:(SDSAnyReadTransaction *)tx
{
    NSMutableArray *errorMessages = [NSMutableArray new];
    [self enumerateRecentInteractionsWithTransaction:tx
                                          usingBlock:^(TSInteraction *interaction) {
                                              if ([interaction isKindOfClass:[TSInvalidIdentityKeyReceivingErrorMessage
                                                                                 class]]) {
                                                  TSInvalidIdentityKeyReceivingErrorMessage *error
                                                      = (TSInvalidIdentityKeyReceivingErrorMessage *)interaction;
                                                  @try {
                                                      if ([[error throws_newIdentityKey] isEqualToData:key]) {
                                                          [errorMessages
                                                              addObject:(TSInvalidIdentityKeyReceivingErrorMessage *)
                                                                            interaction];
                                                      }
                                                  } @catch (NSException *exception) {
                                                      OWSFailDebug(@"exception: %@", exception);
                                                  }
                                              }
                                          }];

    return errorMessages;
}

- (nullable TSInteraction *)lastInteractionForInboxWithTransaction:(SDSAnyReadTransaction *)transaction
{
    OWSAssertDebug(transaction);
    return [[[InteractionFinder alloc] initWithThreadUniqueId:self.uniqueId]
        mostRecentInteractionForInboxWithTransaction:transaction];
}

- (nullable TSInteraction *)firstInteractionAtOrAroundSortId:(uint64_t)sortId
                                                 transaction:(SDSAnyReadTransaction *)transaction
{
    OWSAssertDebug(transaction);
    return
        [[[InteractionFinder alloc] initWithThreadUniqueId:self.uniqueId] firstInteractionAtOrAroundSortId:sortId
                                                                                               transaction:transaction];
}

- (void)updateWithInsertedMessage:(TSInteraction *)message transaction:(SDSAnyWriteTransaction *)transaction
{
    [self updateWithMessage:message wasMessageInserted:YES transaction:transaction];
}

- (void)updateWithUpdatedMessage:(TSInteraction *)message transaction:(SDSAnyWriteTransaction *)transaction
{
    [self updateWithMessage:message wasMessageInserted:NO transaction:transaction];
}

- (uint64_t)messageSortIdForMessage:(TSInteraction *)message transaction:(SDSAnyWriteTransaction *)transaction
{
    if (message.grdbId == nil) {
        OWSFailDebug(@"Missing messageSortId.");
    } else if (message.grdbId.unsignedLongLongValue == 0) {
        OWSFailDebug(@"Invalid messageSortId.");
    } else {
        return message.grdbId.unsignedLongLongValue;
    }
    return 0;
}

- (void)updateWithMessage:(TSInteraction *)message
       wasMessageInserted:(BOOL)wasMessageInserted
              transaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(message != nil);
    OWSAssertDebug(transaction != nil);

    BOOL hasLastVisibleInteraction = [self hasLastVisibleInteractionWithTransaction:transaction];
    BOOL needsToClearLastVisibleSortId = hasLastVisibleInteraction && wasMessageInserted;

    if (![message shouldAppearInInboxWithTransaction:transaction]) {
        // We want to clear the last visible sort ID on any new message,
        // even if the message doesn't appear in the inbox view.
        if (needsToClearLastVisibleSortId) {
            [self clearLastVisibleInteractionWithTransaction:transaction];
        }
        [self scheduleTouchFinalizationWithTransaction:transaction];
        return;
    }

    uint64_t messageSortId = [self messageSortIdForMessage:message transaction:transaction];
    BOOL needsToMarkAsVisible = !self.shouldThreadBeVisible;

    ThreadAssociatedData *associatedData = [ThreadAssociatedData fetchOrDefaultForThread:self transaction:transaction];

    BOOL needsToClearArchived = [self shouldClearArchivedStatusWhenUpdatingWithMessage:message
                                                                    wasMessageInserted:wasMessageInserted
                                                                  threadAssociatedData:associatedData
                                                                           transaction:transaction];

    BOOL needsToUpdateLastInteractionRowId = messageSortId > self.lastInteractionRowId;

    BOOL needsToClearIsMarkedUnread = associatedData.isMarkedUnread && wasMessageInserted;

    if (needsToMarkAsVisible || needsToClearArchived || needsToUpdateLastInteractionRowId
        || needsToClearLastVisibleSortId || needsToClearIsMarkedUnread) {
        [self anyUpdateWithTransaction:transaction
                                 block:^(TSThread *thread) {
                                     thread.shouldThreadBeVisible = YES;
                                     thread.lastInteractionRowId = MAX(thread.lastInteractionRowId, messageSortId);
                                 }];
        [associatedData clearIsArchived:needsToClearArchived
                    clearIsMarkedUnread:needsToClearIsMarkedUnread
                   updateStorageService:YES
                            transaction:transaction];
        if (needsToMarkAsVisible) {
            // Non-visible threads don't get indexed, so if we're becoming visible for the first time...
            [SDSDatabaseStorage.shared touchThread:self shouldReindex:YES transaction:transaction];
        }
        if (needsToClearLastVisibleSortId) {
            [self clearLastVisibleInteractionWithTransaction:transaction];
        }
    } else {
        [self scheduleTouchFinalizationWithTransaction:transaction];
    }
}

- (BOOL)shouldClearArchivedStatusWhenUpdatingWithMessage:(TSInteraction *)message
                                      wasMessageInserted:(BOOL)wasMessageInserted
                                    threadAssociatedData:(ThreadAssociatedData *)threadAssociatedData
                                             transaction:(SDSAnyReadTransaction *)transaction
{
    BOOL needsToClearArchived = threadAssociatedData.isArchived && wasMessageInserted;

    // Shouldn't clear archived during migrations.
    if (!AppContextObjcBridge.CurrentAppContext.isRunningTests && !AppReadiness.isAppReady) {
        needsToClearArchived = NO;
    }

    if ([message isKindOfClass:TSInfoMessage.class]) {
        switch (((TSInfoMessage *)message).messageType) {
            case TSInfoMessageSyncedThread: // Shouldn't clear archived during thread import.
            case TSInfoMessageThreadMerge:
                needsToClearArchived = NO;
                break;
            case TSInfoMessageTypeSessionDidEnd:
            case TSInfoMessageUserNotRegistered:
            case TSInfoMessageTypeUnsupportedMessage:
            case TSInfoMessageTypeGroupUpdate:
            case TSInfoMessageTypeGroupQuit:
            case TSInfoMessageTypeDisappearingMessagesUpdate:
            case TSInfoMessageAddToContactsOffer:
            case TSInfoMessageVerificationStateChange:
            case TSInfoMessageAddUserToProfileWhitelistOffer:
            case TSInfoMessageAddGroupToProfileWhitelistOffer:
            case TSInfoMessageUnknownProtocolVersion:
            case TSInfoMessageUserJoinedSignal:
            case TSInfoMessageProfileUpdate:
            case TSInfoMessagePhoneNumberChange:
            case TSInfoMessageRecipientHidden:
            case TSInfoMessagePaymentsActivationRequest:
            case TSInfoMessagePaymentsActivated:
            case TSInfoMessageSessionSwitchover:
            case TSInfoMessageReportedSpam:
                break;
        }
    }

    // Shouldn't clear archived if:
    // - The thread is muted.
    // - The user has requested we keep muted chats archived.
    // - The message was sent by someone other than the current user. (If the
    //   current user sent the message, we should clear archived.)
    {
        BOOL threadIsMuted = threadAssociatedData.isMuted;
        BOOL shouldKeepMutedChatsArchived = [SSKPreferences shouldKeepMutedChatsArchivedWithTransaction:transaction];
        BOOL wasMessageSentByUs = [message isKindOfClass:[TSOutgoingMessage class]];
        if (threadIsMuted && shouldKeepMutedChatsArchived && !wasMessageSentByUs) {
            needsToClearArchived = NO;
        }
    }

    return needsToClearArchived;
}

- (void)updateWithRemovedMessage:(TSInteraction *)message transaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(message != nil);
    OWSAssertDebug(transaction != nil);

    uint64_t messageSortId = [self messageSortIdForMessage:message transaction:transaction];
    BOOL needsToUpdateLastInteractionRowId = messageSortId == self.lastInteractionRowId;

    NSNumber *_Nullable lastVisibleSortId = [self lastVisibleSortIdWithTransaction:transaction];
    BOOL needsToUpdateLastVisibleSortId
        = (lastVisibleSortId != nil && lastVisibleSortId.unsignedLongLongValue == messageSortId);

    [self updateOnInteractionsRemovedWithNeedsToUpdateLastInteractionRowId:needsToUpdateLastInteractionRowId
                                            needsToUpdateLastVisibleSortId:needsToUpdateLastVisibleSortId
                                                         lastVisibleSortId:lastVisibleSortId
                                                               transaction:transaction];
}

- (void)updateOnInteractionsRemovedWithNeedsToUpdateLastInteractionRowId:(BOOL)needsToUpdateLastInteractionRowId
                                          needsToUpdateLastVisibleSortId:(BOOL)needsToUpdateLastVisibleSortId
                                                             transaction:(SDSAnyWriteTransaction *)transaction
{
    NSNumber *_Nullable lastVisibleSortId = [self lastVisibleSortIdWithTransaction:transaction];

    [self updateOnInteractionsRemovedWithNeedsToUpdateLastInteractionRowId:needsToUpdateLastInteractionRowId
                                            needsToUpdateLastVisibleSortId:needsToUpdateLastVisibleSortId
                                                         lastVisibleSortId:lastVisibleSortId
                                                               transaction:transaction];
}

- (void)updateOnInteractionsRemovedWithNeedsToUpdateLastInteractionRowId:(BOOL)needsToUpdateLastInteractionRowId
                                          needsToUpdateLastVisibleSortId:(BOOL)needsToUpdateLastVisibleSortId
                                                       lastVisibleSortId:(nullable NSNumber *)lastVisibleSortId
                                                             transaction:(SDSAnyWriteTransaction *)transaction
{
    if (needsToUpdateLastInteractionRowId || needsToUpdateLastVisibleSortId) {
        [self anyUpdateWithTransaction:transaction
                                 block:^(TSThread *thread) {
                                     if (needsToUpdateLastInteractionRowId) {
                                         TSInteraction *_Nullable latestInteraction =
                                             [thread lastInteractionForInboxWithTransaction:transaction];
                                         thread.lastInteractionRowId = latestInteraction ? latestInteraction.sortId : 0;
                                     }
                                 }];

        if (needsToUpdateLastVisibleSortId) {
            TSInteraction *_Nullable messageBeforeDeletedMessage =
                [self firstInteractionAtOrAroundSortId:lastVisibleSortId.unsignedLongLongValue transaction:transaction];
            if (messageBeforeDeletedMessage != nil) {
                [self setLastVisibleInteractionWithSortId:messageBeforeDeletedMessage.sortId
                                       onScreenPercentage:1
                                              transaction:transaction];
            } else {
                [self clearLastVisibleInteractionWithTransaction:transaction];
            }
        }
    } else {
        [self scheduleTouchFinalizationWithTransaction:transaction];
    }
}

- (void)scheduleTouchFinalizationWithTransaction:(SDSAnyWriteTransaction *)transactionForMethod
{
    OWSAssertDebug(transactionForMethod != nil);

    // If we insert, update or remove N interactions in a given
    // transactions, we don't need to touch the same thread more
    // than once.
    [transactionForMethod addTransactionFinalizationBlockForKey:self.transactionFinalizationKey
                                                          block:^(SDSAnyWriteTransaction *transactionForBlock) {
                                                              [self.databaseStorage touchThread:self
                                                                                  shouldReindex:NO
                                                                                    transaction:transactionForBlock];
                                                          }];
}

#pragma mark - Archival

+ (BOOL)legacyIsArchivedWithLastMessageDate:(nullable NSDate *)lastMessageDate
                               archivalDate:(nullable NSDate *)archivalDate
{
    if (!archivalDate) {
        return NO;
    }

    if (!lastMessageDate) {
        return YES;
    }

    return [archivalDate compare:lastMessageDate] != NSOrderedAscending;
}

- (void)updateWithDraft:(nullable MessageBody *)draftMessageBody
              replyInfo:(nullable ThreadReplyInfoObjC *)replyInfo
    editTargetTimestamp:(nullable NSNumber *)editTargetTimestamp
            transaction:(SDSAnyWriteTransaction *)transaction
{
    [self anyUpdateWithTransaction:transaction
                             block:^(TSThread *thread) {
                                 thread.messageDraft = draftMessageBody.text;
                                 thread.messageDraftBodyRanges = draftMessageBody.ranges;
                                 thread.editTargetTimestamp = editTargetTimestamp;
                             }];
    if (replyInfo != nil) {
        [replyInfo saveWithThreadUniqueId:self.uniqueId tx:transaction];
    } else {
        [ThreadReplyInfoObjC deleteWithThreadUniqueId:self.uniqueId tx:transaction];
    }
}

- (void)updateWithMentionNotificationMode:(TSThreadMentionNotificationMode)mentionNotificationMode
                      wasLocallyInitiated:(bool)wasLocallyInitiated
                              transaction:(SDSAnyWriteTransaction *)transaction
{
    [self anyUpdateWithTransaction:transaction
                             block:^(TSThread *thread) { thread.mentionNotificationMode = mentionNotificationMode; }];
    if (wasLocallyInitiated && self.isGroupV2Thread) {
        TSGroupThread *groupThread = (TSGroupThread *)self;
        [self.storageServiceManagerObjc recordPendingUpdatesWithGroupModel:groupThread.groupModel];
    }
}

- (void)updateWithShouldThreadBeVisible:(BOOL)shouldThreadBeVisible transaction:(SDSAnyWriteTransaction *)transaction
{
    [self anyUpdateWithTransaction:transaction
                             block:^(TSThread *thread) { thread.shouldThreadBeVisible = shouldThreadBeVisible; }];
}

- (void)updateWithLastSentStoryTimestamp:(nullable NSNumber *)lastSentStoryTimestamp
                             transaction:(SDSAnyWriteTransaction *)transaction
{
    [self anyUpdateWithTransaction:transaction
                             block:^(TSThread *thread) {
                                 if (lastSentStoryTimestamp.unsignedIntegerValue
                                     > thread.lastSentStoryTimestamp.unsignedIntegerValue) {
                                     thread.lastSentStoryTimestamp = lastSentStoryTimestamp;
                                 }
                             }];
}

- (void)updateWithStoryViewMode:(TSThreadStoryViewMode)storyViewMode transaction:(SDSAnyWriteTransaction *)transaction
{
    [self anyUpdateWithTransaction:transaction block:^(TSThread *thread) { thread.storyViewMode = storyViewMode; }];
}

#pragma mark - Merging

- (void)mergeFrom:(TSThread *)otherThread
{
    self.shouldThreadBeVisible = self.shouldThreadBeVisible || otherThread.shouldThreadBeVisible;
    self.lastInteractionRowId = MAX(self.lastInteractionRowId, otherThread.lastInteractionRowId);

    // Copy the draft if this thread doesn't have one. We always assign both
    // values if we assign one of them since they're related.
    if (self.messageDraft == nil) {
        self.messageDraft = otherThread.messageDraft;
        self.messageDraftBodyRanges = otherThread.messageDraftBodyRanges;
    }
}

@end

NS_ASSUME_NONNULL_END
