/*
 Copyright 2015 OpenMarket Ltd
 Copyright 2018 New Vector Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 
 http://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 */

#import "MXKAttachment.h"
#import "MXKSwiftHeader.h"

@import MatrixSDK;
@import MobileCoreServices;

#import "MXKTools.h"

// The size of thumbnail we request from the server
// Note that this is smaller than the ones we upload: when sending, one size
// must fit all, including the web which will want relatively high res thumbnails.
// We, however, are a mobile client and so would prefer smaller thumbnails, which
// we can have if they're being generated by the media repo.
static const int kThumbnailWidth = 320;
static const int kThumbnailHeight = 240;

NSString *const kMXKAttachmentErrorDomain = @"kMXKAttachmentErrorDomain";
NSString *const kMXKAttachmentFileNameBase = @"attatchment";

@interface MXKAttachment ()
{
    /**
     The information on the encrypted content.
     */
    MXEncryptedContentFile *contentFile;
    
    /**
     The information on the encrypted thumbnail.
     */
    MXEncryptedContentFile *thumbnailFile;
    
    /**
     Observe Attachment download
     */
    id onAttachmentDownloadObs;
    
    /**
     The local path used to store the attachment with its original name
     */
    NSString *documentCopyPath;
    
    /**
     The attachment mimetype.
     */
    NSString *mimetype;
}

@end

@implementation MXKAttachment

- (instancetype)initWithEvent:(MXEvent*)event andMediaManager:(MXMediaManager*)mediaManager
{
    self = [super init];
    if (self)
    {
        _mediaManager = mediaManager;
        
        // Make a copy as the data can be read at anytime later
        _eventId = event.eventId;
        _eventRoomId = event.roomId;
        _eventSentState = event.sentState;
        
        NSDictionary *eventContent = event.content;
        
        // Set default thumbnail orientation
        _thumbnailOrientation = UIImageOrientationUp;
        
        if (event.eventType == MXEventTypeSticker)
        {
            _type = MXKAttachmentTypeSticker;
            MXJSONModelSetDictionary(_thumbnailInfo, eventContent[@"info"][@"thumbnail_info"]);
        }
        else
        {
            // Note: mxEvent.eventType is supposed to be MXEventTypeRoomMessage here.
            NSString *msgtype = eventContent[kMXMessageTypeKey];
            if ([msgtype isEqualToString:kMXMessageTypeImage])
            {
                _type = MXKAttachmentTypeImage;
            }
            else if (event.isVoiceMessage)
            {
                _type = MXKAttachmentTypeVoiceMessage;
            }
            else if ([msgtype isEqualToString:kMXMessageTypeAudio])
            {
                _type = MXKAttachmentTypeAudio;
            }
            else if ([msgtype isEqualToString:kMXMessageTypeVideo])
            {
                _type = MXKAttachmentTypeVideo;
                MXJSONModelSetDictionary(_thumbnailInfo, eventContent[@"info"][@"thumbnail_info"]);
            }
            else if ([msgtype isEqualToString:kMXMessageTypeFile])
            {
                _type = MXKAttachmentTypeFile;
            }
            else
            {
                return nil;
            }
        }
        
        MXJSONModelSetString(_originalFileName, eventContent[kMXMessageBodyKey]);
        MXJSONModelSetDictionary(_contentInfo, eventContent[@"info"]);
        MXJSONModelSetMXJSONModel(contentFile, MXEncryptedContentFile, eventContent[@"file"]);
        
        // Retrieve the content url by taking into account the potential encryption.
        if (contentFile)
        {
            _isEncrypted = YES;
            _contentURL = contentFile.url;
            
            MXJSONModelSetMXJSONModel(thumbnailFile, MXEncryptedContentFile, _contentInfo[@"thumbnail_file"]);
        }
        else
        {
            _isEncrypted = NO;
            MXJSONModelSetString(_contentURL, eventContent[@"url"]);
        }
        
        mimetype = nil;
        if (_contentInfo)
        {
            MXJSONModelSetString(mimetype, _contentInfo[@"mimetype"]);
        }
        
        _cacheFilePath = [MXMediaManager cachePathForMatrixContentURI:_contentURL andType:mimetype inFolder:_eventRoomId];
        _downloadId = [MXMediaManager downloadIdForMatrixContentURI:_contentURL inFolder:_eventRoomId];
        
        // Deduce the thumbnail information from the retrieved data.
        _mxcThumbnailURI = [self getThumbnailURI];
        _thumbnailMimeType = [self getThumbnailMimeType];
        _thumbnailCachePath = [self getThumbnailCachePath];
        _thumbnailDownloadId = [self getThumbnailDownloadId];
    }
    return self;
}

- (void)dealloc
{
    [self destroy];
}

- (void)destroy
{
    if (onAttachmentDownloadObs)
    {
        [[NSNotificationCenter defaultCenter] removeObserver:onAttachmentDownloadObs];
        onAttachmentDownloadObs = nil;
    }
    
    // Remove the temporary file created to prepare attachment sharing
    if (documentCopyPath)
    {
        [[NSFileManager defaultManager] removeItemAtPath:documentCopyPath error:nil];
        documentCopyPath = nil;
    }
    
    _previewImage = nil;
}

- (NSString *)getThumbnailURI
{
    if (thumbnailFile)
    {
        // there's an encrypted thumbnail: we return the mxc url
        return thumbnailFile.url;
    }
    
    // Look for a clear thumbnail url
    return _contentInfo[@"thumbnail_url"];
}

- (NSString *)getThumbnailMimeType
{
    return _thumbnailInfo[@"mimetype"];
}

- (NSString*)getThumbnailCachePath
{
    if (_mxcThumbnailURI)
    {
        return [MXMediaManager cachePathForMatrixContentURI:_mxcThumbnailURI andType:_thumbnailMimeType inFolder:_eventRoomId];
    }
    // In case of an unencrypted image, consider the thumbnail URI deduced from the content URL, except if
    // the attachment is currently uploading.
    // Note: When the uploading is in progress, the upload id is stored in the content url (nasty trick).
    else if (_type == MXKAttachmentTypeImage && !_isEncrypted && _contentURL && ![_contentURL hasPrefix:kMXMediaUploadIdPrefix])
    {
        return [MXMediaManager thumbnailCachePathForMatrixContentURI:_contentURL
                                                             andType:@"image/jpeg"
                                                            inFolder:_eventRoomId
                                                       toFitViewSize:CGSizeMake(kThumbnailWidth, kThumbnailHeight)
                                                          withMethod:MXThumbnailingMethodScale];
        
        
    }
    return nil;
}

- (NSString *)getThumbnailDownloadId
{
    if (_mxcThumbnailURI)
    {
        return [MXMediaManager downloadIdForMatrixContentURI:_mxcThumbnailURI inFolder:_eventRoomId];
    }
    // In case of an unencrypted image, consider the thumbnail URI deduced from the content URL, except if
    // the attachment is currently uploading.
    // Note: When the uploading is in progress, the upload id is stored in the content url (nasty trick).
    else if (_type == MXKAttachmentTypeImage && !_isEncrypted && _contentURL && ![_contentURL hasPrefix:kMXMediaUploadIdPrefix])
    {
        return [MXMediaManager thumbnailDownloadIdForMatrixContentURI:_contentURL
                                                             inFolder:_eventRoomId
                                                        toFitViewSize:CGSizeMake(kThumbnailWidth, kThumbnailHeight)
                                                           withMethod:MXThumbnailingMethodScale];
    }
    return nil;
}

- (UIImage *)getCachedThumbnail
{
    if (_thumbnailCachePath)
    {
        UIImage *thumb = [MXMediaManager getFromMemoryCacheWithFilePath:_thumbnailCachePath];
        if (thumb) return thumb;
        
        if ([[NSFileManager defaultManager] fileExistsAtPath:_thumbnailCachePath])
        {
            return [MXMediaManager loadThroughCacheWithFilePath:_thumbnailCachePath];
        }
    }
    return nil;
}

- (void)getThumbnail:(void (^)(MXKAttachment *, UIImage *))onSuccess failure:(void (^)(MXKAttachment *, NSError *error))onFailure
{
    // Check whether a thumbnail is defined.
    if (!_thumbnailCachePath)
    {
        // there is no thumbnail: if we're an image, return the full size image. Otherwise, nothing we can do.
        if (_type == MXKAttachmentTypeImage)
        {
            [self getImage:onSuccess failure:onFailure];
        }
        else if (onFailure)
        {
            onFailure(self, nil);
        }
        
        return;
    }
    
    // Check the current memory cache.
    UIImage *thumb = [MXMediaManager getFromMemoryCacheWithFilePath:_thumbnailCachePath];
    if (thumb)
    {
        onSuccess(self, thumb);
        return;
    }
    
    if (thumbnailFile)
    {
        MXWeakify(self);
        
        void (^decryptAndCache)(void) = ^{
            MXStrongifyAndReturnIfNil(self);
            NSInputStream *instream = [[NSInputStream alloc] initWithFileAtPath:self.thumbnailCachePath];
            NSOutputStream *outstream = [[NSOutputStream alloc] initToMemory];
            [MXEncryptedAttachments decryptAttachment:self->thumbnailFile inputStream:instream outputStream:outstream success:^{
                UIImage *img = [UIImage imageWithData:[outstream propertyForKey:NSStreamDataWrittenToMemoryStreamKey]];
                // Save this image to in-memory cache.
                [MXMediaManager cacheImage:img withCachePath:self.thumbnailCachePath];
                onSuccess(self, img);
            } failure:^(NSError *err) {
                if (err) {
                    MXLogDebug(@"Error decrypting attachment! %@", err.userInfo);
                    if (onFailure) onFailure(self, err);
                    return;
                }
            }];
        };
        
        if ([[NSFileManager defaultManager] fileExistsAtPath:_thumbnailCachePath])
        {
            decryptAndCache();
        }
        else
        {
            [_mediaManager downloadEncryptedMediaFromMatrixContentFile:thumbnailFile
                                                              mimeType:_thumbnailMimeType
                                                              inFolder:_eventRoomId
                                                               success:^(NSString *outputFilePath) {
                                                                   decryptAndCache();
                                                               }
                                                               failure:^(NSError *error) {
                                                                   if (onFailure) onFailure(self, error);
                                                               }];
        }
    }
    else
    {
        if ([[NSFileManager defaultManager] fileExistsAtPath:_thumbnailCachePath])
        {
            onSuccess(self, [MXMediaManager loadThroughCacheWithFilePath:_thumbnailCachePath]);
        }
        else if (_mxcThumbnailURI)
        {
            [_mediaManager downloadMediaFromMatrixContentURI:_mxcThumbnailURI
                                                    withType:_thumbnailMimeType
                                                    inFolder:_eventRoomId
                                                     success:^(NSString *outputFilePath) {
                                                         // Here outputFilePath = thumbnailCachePath
                                                         onSuccess(self, [MXMediaManager loadThroughCacheWithFilePath:outputFilePath]);
                                                     }
                                                     failure:^(NSError *error) {
                                                         if (onFailure) onFailure(self, error);
                                                     }];
        }
        else
        {
            // Here _thumbnailCachePath is defined, so a thumbnail is available.
            // Because _mxcThumbnailURI is null, this means we have to consider the content uri (see getThumbnailCachePath).
            [_mediaManager downloadThumbnailFromMatrixContentURI:_contentURL
                                                         withType:@"image/jpeg"
                                                        inFolder:_eventRoomId
                                                   toFitViewSize:CGSizeMake(kThumbnailWidth, kThumbnailHeight)
                                                      withMethod:MXThumbnailingMethodScale
                                                         success:^(NSString *outputFilePath) {
                                                             // Here outputFilePath = thumbnailCachePath
                                                             onSuccess(self, [MXMediaManager loadThroughCacheWithFilePath:outputFilePath]);
                                                         }
                                                         failure:^(NSError *error) {
                                                             if (onFailure) onFailure(self, error);
                                                         }];
        }
    }
}

- (void)getImage:(void (^)(MXKAttachment *, UIImage *))onSuccess failure:(void (^)(MXKAttachment *, NSError *error))onFailure
{
    [self getAttachmentData:^(NSData *data) {
        
        UIImage *img = [UIImage imageWithData:data];
        
        if (img)
        {
            if (onSuccess)
            {
                onSuccess(self, img);
            }
        }
        else
        {
            if (onFailure)
            {
                NSError *error = [NSError errorWithDomain:kMXKAttachmentErrorDomain code:0 userInfo:@{@"err": @"error_get_image_from_data"}];
                onFailure(self, error);
            }
        }
        
    } failure:^(NSError *error) {
        
        if (onFailure) onFailure(self, error);
        
    }];
}

- (void)getAttachmentData:(void (^)(NSData *))onSuccess failure:(void (^)(NSError *error))onFailure
{
    MXWeakify(self);
    [self prepare:^{
        MXStrongifyAndReturnIfNil(self);
        if (self.isEncrypted)
        {
            // decrypt the encrypted file
            NSInputStream *instream = [[NSInputStream alloc] initWithFileAtPath:self.cacheFilePath];
            NSOutputStream *outstream = [[NSOutputStream alloc] initToMemory];
            [MXEncryptedAttachments decryptAttachment:self->contentFile inputStream:instream outputStream:outstream success:^{
                onSuccess([outstream propertyForKey:NSStreamDataWrittenToMemoryStreamKey]);
            } failure:^(NSError *err) {
                if (err)
                {
                    MXLogDebug(@"Error decrypting attachment! %@", err.userInfo);
                    return;
                }
            }];
        }
        else
        {
            onSuccess([NSData dataWithContentsOfFile:self.cacheFilePath]);
        }
    } failure:onFailure];
}

- (void)decryptToTempFile:(void (^)(NSString *))onSuccess failure:(void (^)(NSError *error))onFailure
{
    MXWeakify(self);
    [self prepare:^{
        MXStrongifyAndReturnIfNil(self);
        NSString *tempPath = [self getTempFile];
        if (!tempPath)
        {
            if (onFailure) onFailure([NSError errorWithDomain:kMXKAttachmentErrorDomain code:0 userInfo:@{@"err": @"error_creating_temp_file"}]);
            return;
        }
        
        NSInputStream *inStream = [NSInputStream inputStreamWithFileAtPath:self.cacheFilePath];
        NSOutputStream *outStream = [NSOutputStream outputStreamToFileAtPath:tempPath append:NO];
        
        [MXEncryptedAttachments decryptAttachment:self->contentFile inputStream:inStream outputStream:outStream success:^{
            onSuccess(tempPath);
        } failure:^(NSError *err) {
            if (err) {
                if (onFailure) onFailure(err);
                return;
            }
        }];
    } failure:onFailure];
}

- (NSString *)getTempFile
{
    // create a file with an appropriate extension because iOS detects based on file extension
    // all over the place
    NSString *ext = [MXTools fileExtensionFromContentType:mimetype];
    NSString *filenameTemplate = [NSString stringWithFormat:@"%@.XXXXXX%@", kMXKAttachmentFileNameBase, ext];
    NSString *template = [NSTemporaryDirectory() stringByAppendingPathComponent:filenameTemplate];
    
    const char *templateCstr = [template fileSystemRepresentation];
    char *tempPathCstr = (char *)malloc(strlen(templateCstr) + 1);
    strcpy(tempPathCstr, templateCstr);
    
    int fd = mkstemps(tempPathCstr, (int)ext.length);
    if (!fd)
    {
        return nil;
    }
    close(fd);
    
    NSString *tempPath = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:tempPathCstr
                                                                                     length:strlen(tempPathCstr)];
    free(tempPathCstr);
    return tempPath;
}

+ (void)clearCache
{
    NSString *temporaryDirectoryPath = NSTemporaryDirectory();
    NSDirectoryEnumerator<NSString *> *enumerator = [NSFileManager.defaultManager enumeratorAtPath:temporaryDirectoryPath];
    
    NSString *filePath;
    while (filePath = [enumerator nextObject]) {
        if(![filePath containsString:kMXKAttachmentFileNameBase]) {
            continue;
        }
        
        NSError *error;
        BOOL result = [NSFileManager.defaultManager removeItemAtPath:[temporaryDirectoryPath stringByAppendingPathComponent:filePath] error:&error];
        if (!result && error) {
            MXLogErrorDetails(@"[MXKAttachment] Failed deleting temporary file with error", @{
                @"error": error ?: @"unknown"
            });
        }
    }
}

- (void)prepare:(void (^)(void))onAttachmentReady failure:(void (^)(NSError *error))onFailure
{
    if ([[NSFileManager defaultManager] fileExistsAtPath:_cacheFilePath])
    {
        // Done
        if (onAttachmentReady)
        {
            onAttachmentReady();
        }
    }
    else
    {
        // Trigger download if it is not already in progress
        MXMediaLoader* loader = [MXMediaManager existingDownloaderWithIdentifier:_downloadId];
        if (!loader)
        {
            if (_isEncrypted)
            {
                loader = [_mediaManager downloadEncryptedMediaFromMatrixContentFile:contentFile
                                                                           mimeType:mimetype
                                                                           inFolder:_eventRoomId];
            }
            else
            {
                loader = [_mediaManager downloadMediaFromMatrixContentURI:_contentURL
                                                                 withType:mimetype
                                                                 inFolder:_eventRoomId];
            }
        }
        
        if (loader)
        {
            MXWeakify(self);
            
            // Add observers
            onAttachmentDownloadObs = [[NSNotificationCenter defaultCenter] addObserverForName:kMXMediaLoaderStateDidChangeNotification object:loader queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
                
                MXStrongifyAndReturnIfNil(self);
                MXMediaLoader *loader = (MXMediaLoader*)notif.object;
                switch (loader.state) {
                    case MXMediaLoaderStateDownloadCompleted:
                        [[NSNotificationCenter defaultCenter] removeObserver:self->onAttachmentDownloadObs];
                        self->onAttachmentDownloadObs = nil;
                        if (onAttachmentReady)
                        {
                            onAttachmentReady ();
                        }
                        break;
                    case MXMediaLoaderStateDownloadFailed:
                    case MXMediaLoaderStateCancelled:
                        [[NSNotificationCenter defaultCenter] removeObserver:self->onAttachmentDownloadObs];
                        self->onAttachmentDownloadObs = nil;
                        if (onFailure)
                        {
                            onFailure (loader.error);
                        }
                        break;
                    default:
                        break;
                }
            }];
        }
        else if (onFailure)
        {
            onFailure (nil);
        }
    }
}

- (void)save:(void (^)(void))onSuccess failure:(void (^)(NSError *error))onFailure
{
    if (_type == MXKAttachmentTypeImage || _type == MXKAttachmentTypeVideo)
    {
        MXWeakify(self);
        if (self.isEncrypted) {
            [self decryptToTempFile:^(NSString *path) {
                MXStrongifyAndReturnIfNil(self);
                NSURL* url = [NSURL fileURLWithPath:path];
                
                [MXMediaManager saveMediaToPhotosLibrary:url
                                                  isImage:(self.type == MXKAttachmentTypeImage)
                                                  success:^(NSURL *assetURL){
                                                      if (onSuccess)
                                                      {
                                                          [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
                                                          onSuccess();
                                                      }
                                                  }
                                                  failure:onFailure];
            } failure:onFailure];
        }
        else
        {
            [self prepare:^{
                MXStrongifyAndReturnIfNil(self);
                NSURL* url = [NSURL fileURLWithPath:self.cacheFilePath];
                
                [MXMediaManager saveMediaToPhotosLibrary:url
                                                  isImage:(self.type == MXKAttachmentTypeImage)
                                                  success:^(NSURL *assetURL){
                                                      if (onSuccess)
                                                      {
                                                          onSuccess();
                                                      }
                                                  }
                                                  failure:onFailure];
            } failure:onFailure];
        }
    }
    else
    {
        // Not supported
        if (onFailure)
        {
            onFailure(nil);
        }
    }
}

- (void)copy:(void (^)(void))onSuccess failure:(void (^)(NSError *error))onFailure
{
    MXWeakify(self);
    [self prepare:^{
        MXStrongifyAndReturnIfNil(self);
        if (self.type == MXKAttachmentTypeImage)
        {
            [self getImage:^(MXKAttachment *attachment, UIImage *img) {
                MXKPasteboardManager.shared.pasteboard.image = img;
                if (onSuccess)
                {
                    onSuccess();
                }
            } failure:^(MXKAttachment *attachment, NSError *error) {
                if (onFailure) onFailure(error);
            }];
        }
        else
        {
            MXWeakify(self);
            [self getAttachmentData:^(NSData *data) {
                if (data)
                {
                    MXStrongifyAndReturnIfNil(self);
                    NSString* UTI = (__bridge_transfer NSString *) UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[self.cacheFilePath pathExtension] , NULL);
                    
                    if (UTI)
                    {
                        [MXKPasteboardManager.shared.pasteboard setData:data forPasteboardType:UTI];
                        if (onSuccess)
                        {
                            onSuccess();
                        }
                    }
                }
            } failure:onFailure];
        }
        
        // Unexpected error
        if (onFailure)
        {
            onFailure(nil);
        }
        
    } failure:onFailure];
}

- (MXKUTI *)uti
{
    return [[MXKUTI alloc] initWithMimeType:mimetype];
}

- (void)prepareShare:(void (^)(NSURL *fileURL))onReadyToShare failure:(void (^)(NSError *error))onFailure
{
    MXWeakify(self);
    void (^haveFile)(NSString *) = ^(NSString *path) {
        // Prepare the file URL by considering the original file name (if any)
        NSURL *fileUrl;
        MXStrongifyAndReturnIfNil(self);
        // Check whether the original name retrieved from event body has extension
        if (self.originalFileName && [self.originalFileName pathExtension].length)
        {
            // Copy the cached file to restore its original name
            // Note:  We used previously symbolic link (instead of copy) but UIDocumentInteractionController failed to open Office documents (.docx, .pptx...).
            self->documentCopyPath = [[MXMediaManager getCachePath] stringByAppendingPathComponent:self.originalFileName];
            
            [[NSFileManager defaultManager] removeItemAtPath:self->documentCopyPath error:nil];
            if ([[NSFileManager defaultManager] copyItemAtPath:path toPath:self->documentCopyPath error:nil])
            {
                fileUrl = [NSURL fileURLWithPath:self->documentCopyPath];
                [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
            }
        }
        
        if (!fileUrl)
        {
            // Use the cached file by default
            fileUrl = [NSURL fileURLWithPath:path];
            self->documentCopyPath = path;
        }
        
        onReadyToShare (fileUrl);
    };
    
    if (self.isEncrypted)
    {
        [self decryptToTempFile:^(NSString *path) {
            haveFile(path);
        } failure:onFailure];
    }
    else
    {
        // First download data if it is not already done
        [self prepare:^{
            haveFile(self.cacheFilePath);
        } failure:onFailure];
    }
}

- (void)onShareEnded
{
    // Remove the temporary file created to prepare attachment sharing
    if (documentCopyPath)
    {
        [[NSFileManager defaultManager] removeItemAtPath:documentCopyPath error:nil];
        documentCopyPath = nil;
    }
}

@end
