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

#import "TSAttachmentPointer.h"
#import "TSAttachmentStream.h"
#import <SignalServiceKit/SignalServiceKit-Swift.h>

NS_ASSUME_NONNULL_BEGIN

NSString *NSStringForTSAttachmentPointerState(TSAttachmentPointerState value)
{
    switch (value) {
        case TSAttachmentPointerStateEnqueued:
            return @"Enqueued";
        case TSAttachmentPointerStateDownloading:
            return @"Downloading";
        case TSAttachmentPointerStateFailed:
            return @"Failed";
        case TSAttachmentPointerStatePendingMessageRequest:
            return @"PendingMessageRequest";
        case TSAttachmentPointerStatePendingManualDownload:
            return @"PendingManualDownload";
        default:
            OWSCFailDebug(@"Invalid value.");
            return @"Invalid value";
    }
}

static const NSUInteger kMaxAttachmentsPerDataMessage = 100;

@interface TSAttachmentStream (TSAttachmentPointer)

- (CGSize)cachedMediaSize;

@end

#pragma mark -

@interface TSAttachmentPointer ()

@property (nonatomic) TSAttachmentPointerState state;

// Optional property.  Only set for attachments which need "lazy backup restore."
@property (nonatomic, nullable) NSString *lazyRestoreFragmentId;

@end

#pragma mark -

@implementation TSAttachmentPointer

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

    // A TSAttachmentPointer is a yet-to-be-downloaded attachment.
    // If this is an old TSAttachmentPointer from another session,
    // we know that it failed to complete before the session completed.
    if (![coder containsValueForKey:@"state"]) {
        _state = TSAttachmentPointerStateFailed;
    }

    // Put a meaningless value into the meaningless field
    _pointerType = 0;

    return self;
}

- (instancetype)initWithServerId:(UInt64)serverId
                          cdnKey:(NSString *)cdnKey
                       cdnNumber:(UInt32)cdnNumber
                             key:(NSData *)key
                          digest:(nullable NSData *)digest
                       byteCount:(UInt32)byteCount
                     contentType:(NSString *)contentType
                      clientUuid:(NSUUID *)clientUuid
                  sourceFilename:(nullable NSString *)sourceFilename
                         caption:(nullable NSString *)caption
                  albumMessageId:(nullable NSString *)albumMessageId
                  attachmentType:(TSAttachmentType)attachmentType
                       mediaSize:(CGSize)mediaSize
                        blurHash:(nullable NSString *)blurHash
                 uploadTimestamp:(unsigned long long)uploadTimestamp
                   videoDuration:(nullable NSNumber *)videoDuration
{
    self = [super initWithServerId:serverId
                            cdnKey:cdnKey
                         cdnNumber:cdnNumber
                     encryptionKey:key
                         byteCount:byteCount
                       contentType:contentType
                        clientUuid:clientUuid
                    sourceFilename:sourceFilename
                           caption:caption
                    attachmentType:attachmentType
                    albumMessageId:albumMessageId
                          blurHash:blurHash
                   uploadTimestamp:uploadTimestamp
                     videoDuration:videoDuration];
    if (!self) {
        return self;
    }

    _digest = digest;
    _state = TSAttachmentPointerStateEnqueued;
    // Put a meaningless value into the meaningless field
    _pointerType = 0;
    _mediaSize = mediaSize;

    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
                  albumMessageId:(nullable NSString *)albumMessageId
         attachmentSchemaVersion:(NSUInteger)attachmentSchemaVersion
                  attachmentType:(TSAttachmentType)attachmentType
                        blurHash:(nullable NSString *)blurHash
                       byteCount:(unsigned int)byteCount
                         caption:(nullable NSString *)caption
                          cdnKey:(NSString *)cdnKey
                       cdnNumber:(unsigned int)cdnNumber
                      clientUuid:(nullable NSString *)clientUuid
                     contentType:(NSString *)contentType
                   encryptionKey:(nullable NSData *)encryptionKey
                        serverId:(unsigned long long)serverId
                  sourceFilename:(nullable NSString *)sourceFilename
                 uploadTimestamp:(unsigned long long)uploadTimestamp
                   videoDuration:(nullable NSNumber *)videoDuration
                          digest:(nullable NSData *)digest
           lazyRestoreFragmentId:(nullable NSString *)lazyRestoreFragmentId
                       mediaSize:(CGSize)mediaSize
                     pointerType:(NSUInteger)pointerType
                           state:(TSAttachmentPointerState)state
{
    self = [super initWithGrdbId:grdbId
                        uniqueId:uniqueId
                    albumMessageId:albumMessageId
           attachmentSchemaVersion:attachmentSchemaVersion
                    attachmentType:attachmentType
                          blurHash:blurHash
                         byteCount:byteCount
                           caption:caption
                            cdnKey:cdnKey
                         cdnNumber:cdnNumber
                        clientUuid:clientUuid
                       contentType:contentType
                     encryptionKey:encryptionKey
                          serverId:serverId
                    sourceFilename:sourceFilename
                   uploadTimestamp:uploadTimestamp
                     videoDuration:videoDuration];

    if (!self) {
        return self;
    }

    _digest = digest;
    _lazyRestoreFragmentId = lazyRestoreFragmentId;
    _mediaSize = mediaSize;
    _pointerType = pointerType;
    _state = state;

    [self sdsFinalizeAttachmentPointer];

    return self;
}

// clang-format on

// --- CODE GENERATION MARKER

- (void)sdsFinalizeAttachmentPointer
{
    [self upgradeAttachmentSchemaVersionIfNecessary];
}

+ (nullable TSAttachmentPointer *)attachmentPointerFromProto:(SSKProtoAttachmentPointer *)attachmentProto
                                                albumMessage:(nullable TSMessage *)albumMessage
{
    // TODO: Use the cdnKey and cdnNumber to fetch the attachment
    if (attachmentProto.cdnID < 1 && attachmentProto.cdnKey.length < 1) {
        OWSFailDebug(@"Invalid attachment id.");
        return nil;
    }
    if (attachmentProto.key.length < 1) {
        OWSFailDebug(@"Invalid attachment key.");
        return nil;
    }

    NSUUID *_Nullable clientUuid;
    if (attachmentProto.hasClientUuid) {
        clientUuid = [NSUUID fromData:attachmentProto.clientUuid];
    } else {
        clientUuid = nil;
    }

    NSString *_Nullable fileName = attachmentProto.fileName;
    NSString *_Nullable contentType = attachmentProto.contentType;
    if (contentType.length < 1) {
        // Content type might not set if the sending client can't
        // infer a MIME type from the file extension.
        OWSLogWarn(@"Invalid attachment content type.");
        NSString *_Nullable fileExtension = [fileName pathExtension].lowercaseString;
        if (fileExtension.length > 0) {
            contentType = [MimeTypeUtil mimeTypeForFileExtension:fileExtension];
        }
        if (contentType.length < 1) {
            contentType = MimeTypeUtil.mimeTypeApplicationOctetStream;
        }
    }

    // digest will be empty for old clients.
    NSData *_Nullable digest = attachmentProto.hasDigest ? attachmentProto.digest : nil;

    TSAttachmentType attachmentType = TSAttachmentTypeDefault;
    if ([attachmentProto hasFlags]) {
        UInt32 flags = attachmentProto.flags;
        if ((flags & SSKProtoAttachmentPointerFlagsVoiceMessage) > 0) {
            attachmentType = TSAttachmentTypeVoiceMessage;
        } else if ((flags & SSKProtoAttachmentPointerFlagsBorderless) > 0) {
            attachmentType = TSAttachmentTypeBorderless;
        } else if ((flags & SSKProtoAttachmentPointerFlagsGif) > 0) {
            attachmentType = TSAttachmentTypeGIF;
        }
    }
    NSString *_Nullable caption;
    if (attachmentProto.hasCaption) {
        caption = attachmentProto.caption;
    }

    NSString *_Nullable albumMessageId;
    if (albumMessage != nil) {
        albumMessageId = albumMessage.uniqueId;
    }

    CGSize mediaSize = CGSizeZero;
    if (attachmentProto.hasWidth && attachmentProto.hasHeight && attachmentProto.width > 0
        && attachmentProto.height > 0) {
        mediaSize = CGSizeMake(attachmentProto.width, attachmentProto.height);
    }

    UInt64 serverId = attachmentProto.cdnID;
    if (![SDS fitsInInt64:serverId]) {
        OWSFailDebug(@"Invalid server id.");
        return nil;
    }

    NSString *_Nullable blurHash;
    if (contentType.length > 0 && [MimeTypeUtil isSupportedVisualMediaMimeType:contentType]
        && attachmentProto.hasBlurHash) {
        blurHash = attachmentProto.blurHash;
        if (![BlurHash isValidBlurHash:blurHash]) {
            blurHash = nil;
        }
    }

    unsigned long long uploadTimestamp = 0;
    if (attachmentProto.hasUploadTimestamp) {
        uploadTimestamp = attachmentProto.uploadTimestamp;
    }

    TSAttachmentPointer *pointer = [[TSAttachmentPointer alloc] initWithServerId:serverId
                                                                          cdnKey:attachmentProto.cdnKey
                                                                       cdnNumber:attachmentProto.cdnNumber
                                                                             key:attachmentProto.key
                                                                          digest:digest
                                                                       byteCount:attachmentProto.size
                                                                     contentType:contentType
                                                                      clientUuid:clientUuid
                                                                  sourceFilename:fileName
                                                                         caption:caption
                                                                  albumMessageId:albumMessageId
                                                                  attachmentType:attachmentType
                                                                       mediaSize:mediaSize
                                                                        blurHash:blurHash
                                                                 uploadTimestamp:uploadTimestamp
                                                                   videoDuration:nil];
    return pointer;
}

+ (NSArray<TSAttachmentPointer *> *)attachmentPointersFromProtos:
                                        (NSArray<SSKProtoAttachmentPointer *> *)attachmentProtos
                                                    albumMessage:(TSMessage *)albumMessage
{
    OWSAssertDebug(attachmentProtos);
    OWSAssertDebug(albumMessage);

    NSUInteger pointerCount = MIN(kMaxAttachmentsPerDataMessage, attachmentProtos.count);

    NSMutableArray *attachmentPointers = [[NSMutableArray alloc] initWithCapacity:pointerCount];
    for (NSUInteger i = 0; i < pointerCount; i = i + 1) {
        SSKProtoAttachmentPointer *attachmentProto = attachmentProtos[i];
        TSAttachmentPointer *_Nullable attachmentPointer = [self attachmentPointerFromProto:attachmentProto
                                                                               albumMessage:albumMessage];
        if (attachmentPointer) {
            [attachmentPointers addObject:attachmentPointer];
        }
    }
    return [attachmentPointers copy];
}

#pragma mark - Update With... Methods

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

    [self checkForStreamOverwrite:transaction];
}

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

    [self checkForStreamOverwrite:transaction];
}

- (void)checkForStreamOverwrite:(SDSAnyReadTransaction *)transaction
{
#ifdef DEBUG
    if (self.uniqueId.length > 0) {
        TSAttachment *_Nullable oldObject = [TSAttachment anyFetchWithUniqueId:self.uniqueId transaction:transaction];
        if ([oldObject isKindOfClass:[TSAttachmentStream class]]) {
            OWSFailDebug(@"We should never overwrite a TSAttachmentStream with a TSAttachmentPointer.");
        }
    } else {
        OWSFailDebug(@"Missing uniqueId.");
    }
#endif
}

#if TESTABLE_BUILD
- (void)setAttachmentPointerStateDebug:(TSAttachmentPointerState)state
{
    self.state = state;
}
#endif

- (void)updateWithAttachmentPointerState:(TSAttachmentPointerState)state
                             transaction:(SDSAnyWriteTransaction *)transaction
{
    [self anyUpdateAttachmentPointerWithTransaction:transaction
                                              block:^(TSAttachmentPointer *attachmentPointer) {
                                                  attachmentPointer.state = state;
                                              }];
}

- (void)updateAttachmentPointerStateFrom:(TSAttachmentPointerState)oldState
                                      to:(TSAttachmentPointerState)newState
                             transaction:(SDSAnyWriteTransaction *)transaction
{
    [self anyUpdateAttachmentPointerWithTransaction:transaction
                                              block:^(TSAttachmentPointer *attachmentPointer) {
                                                  if (attachmentPointer.state == oldState) {
                                                      attachmentPointer.state = newState;
                                                  }
                                              }];
}

@end

NS_ASSUME_NONNULL_END
