#import "TGCache.h"

#import <objc/runtime.h>
#import <CommonCrypto/CommonDigest.h>
#import <ImageIO/ImageIO.h>

#import <pthread.h>

#import "TGImageUtils.h"

#import "TGAppDelegate.h"

#define TG_SYNCHRONIZED_DEFINE(lock) pthread_mutex_t TG_SYNCHRONIZED_##lock
#define TG_SYNCHRONIZED_INIT(lock) pthread_mutex_init(&TG_SYNCHRONIZED_##lock, NULL)
#define TG_SYNCHRONIZED_BEGIN(lock) pthread_mutex_lock(&TG_SYNCHRONIZED_##lock);
#define TG_SYNCHRONIZED_END(lock) pthread_mutex_unlock(&TG_SYNCHRONIZED_##lock);

static NSString *md5String(NSString *string)
{
    /*static const char *md5PropertyKey = "MD5Key";
    NSString *result = objc_getAssociatedObject(string, md5PropertyKey);
    if (result != nil)
        return result;*/
    
    const char *ptr = [string UTF8String];
    unsigned char md5Buffer[16];
    CC_MD5(ptr, (CC_LONG)[string lengthOfBytesUsingEncoding:NSUTF8StringEncoding], md5Buffer);
    NSString *output = [[NSString alloc] initWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", md5Buffer[0], md5Buffer[1], md5Buffer[2], md5Buffer[3], md5Buffer[4], md5Buffer[5], md5Buffer[6], md5Buffer[7], md5Buffer[8], md5Buffer[9], md5Buffer[10], md5Buffer[11], md5Buffer[12], md5Buffer[13], md5Buffer[14], md5Buffer[15]];
    //objc_setAssociatedObject(string, md5PropertyKey, output, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return output;
}

@interface TGCacheRecord : NSObject

@property (nonatomic) NSTimeInterval date;
@property (nonatomic, strong) id object;
@property (nonatomic) NSUInteger size;

- (id)initWithObject:(id)object size:(NSUInteger)size;

@end

@implementation TGCacheRecord

@synthesize date = _date;
@synthesize object = _object;
@synthesize size = _size;

- (id)initWithObject:(id)object size:(NSUInteger)size
{
    self = [super init];
    if (self != nil)
    {
        _object = object;
        _date = CFAbsoluteTimeGetCurrent();
        _size = size;
    }
    return self;
}

@end

static NSFileManager *cacheFileManager = nil;

@interface TGCache ()
{
    TG_SYNCHRONIZED_DEFINE(_dataMemoryCache);
}

@property (nonatomic, strong) NSMutableArray *temporaryCachedImagesSources;

@property (nonatomic, strong) NSMutableDictionary *memoryCache;
@property (nonatomic) int memoryCacheSize1;

@property (nonatomic, strong) NSMutableDictionary *thumbnailCache;
@property (nonatomic) int thumbnailCacheSize;

@property (nonatomic, strong) NSMutableDictionary *dataMemoryCache;
@property (nonatomic) int dataMemoryCacheSize;

@end

@implementation TGCache

+ (dispatch_queue_t)diskCacheQueue
{
    static dispatch_queue_t queue = NULL;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^
    {
        queue = dispatch_queue_create("com.telegraph.diskcache", 0);
        //dispatch_set_target_queue(queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0));
        
        if (cacheFileManager == nil)
            cacheFileManager = [[NSFileManager alloc] init];
    });
    return queue;
}

+ (NSFileManager *)diskFileManager
{
    if (cacheFileManager == nil)
        cacheFileManager = [[NSFileManager alloc] init];
    
    return cacheFileManager;
}

- (id)init
{
    self = [super init];
    if (self != nil)
    {
        TG_SYNCHRONIZED_INIT(_dataMemoryCache);
        
        _imageMemoryLimit = deviceMemorySize() > 300 ? (int)(15 * 1024 * 1024) : (int)(11 * 1024 * 1024);
        _imageMemoryEvictionInterval = deviceMemorySize() > 300 ? 1024 * 1024 : 812 * 1024;
        
        //_imageMemoryLimit = 10;
        //_imageMemoryEvictionInterval = 10;
        
        _thumbnailMemoryLimit = deviceMemorySize() > 300 ? (int)(1.6 * 1024 * 1024) : (int)(1.1 * 1024 * 1024);
        _thumbnailEvictionInterval = cpuCoreCount() > 1 ? (int)(0.4 * 1024 * 1024) : (int)(0.25 * 1024 * 1024);
        
        _dataMemoryLimit = deviceMemorySize() > 300 ? (int)(1 * 1024 * 1024) : (int)(0.6 * 1024 * 1024);
        _dataMemoryEvictionInterval = cpuCoreCount() > 1 ? (int)(0.4 * 1024 * 1024) : (int)(0.25 * 1024 * 1024);
        
        _diskLimit = 32 * 1024 * 1024;
        _diskEvictionInterval = 6 * 1024 * 1024;
        
        _memoryWarningBaseline = deviceMemorySize() > 300 ? (int)(1.5 * 1024 * 1024) : (int)(1.1 * 1024 * 1024);
        
        _backgroundBaseline = deviceMemorySize() > 300 ? (int)(5.8 * 1024 * 1024) : (int)(2.8 * 1024 * 1024);
        
        _temporaryCachedImagesSources = [[NSMutableArray alloc] init];
        
        _memoryCache = [[NSMutableDictionary alloc] init];
        self.memoryCacheSize = 0;
        
        _thumbnailCache = [[NSMutableDictionary alloc] init];
        _thumbnailCacheSize = 0;
        
        _dataMemoryCache = [[NSMutableDictionary alloc] init];
        _dataMemoryCacheSize = 0;
        
        NSString *cachesPath = [TGAppDelegate cachePath];
        
        _diskCachePath = cachesPath;
        NSFileManager *fileManager = [[NSFileManager alloc] init];
        if (![fileManager fileExistsAtPath:_diskCachePath])
            [fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
        
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
    }
    return self;
}

- (int)memoryCacheSize
{
    return _memoryCacheSize1;
}

- (void)setMemoryCacheSize:(int)memoryCacheSize
{
    _memoryCacheSize1 = memoryCacheSize;
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)didReceiveMemoryWarning:(NSNotification *)__unused notification
{
    [self freeMemoryCache:_memoryWarningBaseline];
}

- (void)didEnterBackground:(NSNotification *)__unused notification
{
    [self freeMemoryCache:_backgroundBaseline];
}

- (void)addTemporaryCachedImagesSource:(NSDictionary *)source autoremove:(bool)autoremove
{
    dispatch_block_t block = ^
    {
        [_temporaryCachedImagesSources addObject:source];
        if (autoremove)
        {
            dispatch_async(dispatch_get_main_queue(), ^{
                [self removeTemporaryCachedImageSource:source];
            });
        }
    };
    
    if ([NSThread isMainThread])
        block();
    else
        dispatch_async(dispatch_get_main_queue(), block);
}

- (void)removeTemporaryCachedImageSource:(NSDictionary *)source
{
    dispatch_block_t block = ^
    {
        [_temporaryCachedImagesSources removeObject:source];
    };
    
    if ([NSThread isMainThread])
        block();
    else
        dispatch_async(dispatch_get_main_queue(), block);
}

- (void)freeMemoryCache:(NSUInteger)targetSize
{
    dispatch_block_t block = ^
    {
        if (self.memoryCacheSize > (int)targetSize)
        {
            __unused int sizeBefore = self.memoryCacheSize;
            NSArray *sortedKeys = [_memoryCache keysSortedByValueUsingComparator:^NSComparisonResult(id obj1, id obj2)
            {
                return (((TGCacheRecord *)obj1).date < ((TGCacheRecord *)obj2).date) ? NSOrderedAscending : NSOrderedDescending;
            }];
            for (int i = 0; i < (int)sortedKeys.count && self.memoryCacheSize > (int)targetSize; i++)
            {
                NSString *key = [sortedKeys objectAtIndex:i];
                TGCacheRecord *record = [_memoryCache objectForKey:key];
                self.memoryCacheSize -= (int)record.size;
                if (self.memoryCacheSize < 0)
                    self.memoryCacheSize = 0;
                [_memoryCache removeObjectForKey:key];
                //TGLog(@"evict %@", key);
            }
            
            __block int currentCacheSize = 0;
            [_memoryCache enumerateKeysAndObjectsUsingBlock:^(__unused NSString *key, TGCacheRecord *record, __unused BOOL *stop)
            {
                currentCacheSize += record.size;
            }];
            
            self.memoryCacheSize = currentCacheSize;
            
            //TGLog(@"TGCache: freed %d kbytes (cache size: %d kbytes)", (int)((sizeBefore - self.memoryCacheSize) / 1024), (int)(self.memoryCacheSize / 1024));
        }
    };
    if ([NSThread isMainThread])
        block();
    else
        dispatch_async(dispatch_get_main_queue(), block);
}

- (void)freeThumbnailCache:(NSUInteger)targetSize
{
    dispatch_block_t block = ^
    {
        if (_thumbnailCacheSize > (int)targetSize)
        {
            //int sizeBefore = _thumbnailCacheSize;
            NSArray *sortedKeys = [_thumbnailCache keysSortedByValueUsingComparator:^NSComparisonResult(id obj1, id obj2)
            {
                return (((TGCacheRecord *)obj1).date < ((TGCacheRecord *)obj2).date) ? NSOrderedAscending : NSOrderedDescending;
            }];
            for (int i = 0; i < (int)sortedKeys.count && _thumbnailCacheSize > (int)targetSize; i++)
            {
                NSString *key = [sortedKeys objectAtIndex:i];
                TGCacheRecord *record = [_thumbnailCache objectForKey:key];
                _thumbnailCacheSize -= record.size;
                if (_thumbnailCacheSize < 0)
                    _thumbnailCacheSize = 0;
                [_thumbnailCache removeObjectForKey:key];
            }
        }
    };
    if ([NSThread isMainThread])
        block();
    else
        dispatch_async(dispatch_get_main_queue(), block);
}

- (void)freeCompressedMemoryCache:(NSUInteger)targetSize reentrant:(bool)reentrant
{
    if (!reentrant)
        TG_SYNCHRONIZED_BEGIN(_dataMemoryCache);
    {
        if (_dataMemoryCacheSize > (int)targetSize)
        {
            __unused int sizeBefore = _dataMemoryCacheSize;
            NSArray *sortedKeys = [_dataMemoryCache keysSortedByValueUsingComparator:^NSComparisonResult(id obj1, id obj2)
            {
                return (((TGCacheRecord *)obj1).date < ((TGCacheRecord *)obj2).date) ? NSOrderedAscending : NSOrderedDescending;
            }];
            for (int i = 0; i < (int)sortedKeys.count && _dataMemoryCacheSize > (int)targetSize; i++)
            {
                NSString *key = [sortedKeys objectAtIndex:i];
                TGCacheRecord *record = [_dataMemoryCache objectForKey:key];
                _dataMemoryCacheSize -= record.size;
                if (_dataMemoryCacheSize < 0)
                    _dataMemoryCacheSize = 0;
                [_dataMemoryCache removeObjectForKey:key];
            }
            //TGLog(@"TGCache (compressed): freed %d kbytes (cache size: %d kbytes)", (int)((sizeBefore - _dataMemoryCacheSize) / 1024), (int)(_dataMemoryCacheSize / 1024));
        }
    }
    if (!reentrant)
        TG_SYNCHRONIZED_END(_dataMemoryCache);
}

- (void)cacheCompressedObject:(NSData *)data url:(NSString *)url reentrant:(bool)reentrant
{
    if (!reentrant)
        TG_SYNCHRONIZED_BEGIN(_dataMemoryCache);
    
    TGCacheRecord *cacheRecord = [_dataMemoryCache objectForKey:url];
    if (cacheRecord != nil)
    {
        _dataMemoryCacheSize -= cacheRecord.size;
        cacheRecord.date = CFAbsoluteTimeGetCurrent();
        cacheRecord.object = data;
        cacheRecord.size = data.length;
        _dataMemoryCacheSize += cacheRecord.size;
    }
    else
    {
        [_dataMemoryCache setObject:[[TGCacheRecord alloc] initWithObject:data size:data.length] forKey:url];
        _dataMemoryCacheSize += data.length;
    }
    
    if (_dataMemoryCacheSize >= _dataMemoryLimit + _dataMemoryEvictionInterval)
        [self freeCompressedMemoryCache:_dataMemoryLimit reentrant:true];
    
    if (!reentrant)
        TG_SYNCHRONIZED_END(_dataMemoryCache);
}

- (void)cacheImage:(UIImage *)image withData:(NSData *)data url:(NSString *)url availability:(int)availability
{
    [self cacheImage:image withData:data url:url availability:availability completion:nil];
}

- (void)cacheImage:(UIImage *)image withData:(NSData *)data url:(NSString *)url availability:(int)availability completion:(void (^)(void))completion
{
#ifdef DEBUG
    if (data != nil)
        TGLog(@"cache image %d bytes with url %@", (int)data.length, url);
#endif
    
    if (image != nil && (availability & TGCacheMemory))
    {
        int size = (int)(image.size.width * image.size.height * 4 * image.scale);
        dispatch_block_t block = ^
        {
            TGCacheRecord *cacheRecord = [_memoryCache objectForKey:url];
            if (cacheRecord != nil)
            {
                self.memoryCacheSize -= (int)cacheRecord.size;
                cacheRecord.date = CFAbsoluteTimeGetCurrent();
                cacheRecord.object = image;
                cacheRecord.size = size;
                self.memoryCacheSize += size;
            }
            else
            {
                [_memoryCache setObject:[[TGCacheRecord alloc] initWithObject:image size:size] forKey:url];
                self.memoryCacheSize += size;
            }
            
            if (self.memoryCacheSize >= _imageMemoryLimit + _imageMemoryEvictionInterval)
                [self freeMemoryCache:_imageMemoryLimit];
        };
        if ([NSThread isMainThread])
            block();
        else
            dispatch_async(dispatch_get_main_queue(), block);
    }
    
    if ((data != nil || image != nil) && (availability & TGCacheDisk) && url != nil)
    {
        dispatch_async([TGCache diskCacheQueue], ^
        {   
            if (data != nil)
            {
                [self cacheCompressedObject:data url:url reentrant:false];

                [data writeToFile:[_diskCachePath stringByAppendingPathComponent:md5String(url)] atomically:true];
                
                if (completion != nil)
                    completion();
            }
        });
    }
}

- (UIImage *)cachedImage:(NSString *)url availability:(int)availability
{
    UIImage *image = nil;
    
    if (availability & TGCacheMemory)
    {
        __block UIImage *blockImage = nil;
        dispatch_block_t block = ^
        {
            TGCacheRecord *cacheRecord = [_memoryCache objectForKey:url];
            if (cacheRecord != nil)
            {
                cacheRecord.date = CFAbsoluteTimeGetCurrent();
                blockImage = cacheRecord.object;
            }
            else if (_temporaryCachedImagesSources.count != 0)
            {
                for (NSDictionary *dict in _temporaryCachedImagesSources)
                {
                    UIImage *image = [dict objectForKey:url];
                    //TGLog(@"From temp cache %@", url);
                    if (image != nil)
                    {
                        blockImage = image;
                        break;
                    }
                }
            }
        };
        if ([NSThread isMainThread])
            block();
        else
            dispatch_sync(dispatch_get_main_queue(), block);
        image = blockImage;
    }
    
    if (image != nil)
        return image;
    
    if (availability & TGCacheDisk)
    {
        UIImage *dataImage = nil;
        
        TG_SYNCHRONIZED_BEGIN(_dataMemoryCache);
        {
            TGCacheRecord *cacheRecord = [_dataMemoryCache objectForKey:url];
            if (cacheRecord != nil)
            {
                cacheRecord.date = CFAbsoluteTimeGetCurrent();
                
                if (false)
                {
                    dataImage = [[UIImage alloc] initWithData:cacheRecord.object];
                }
                else
                {
                    NSDictionary *dict = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:true] forKey:(id)kCGImageSourceShouldCache];
                    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)cacheRecord.object, nil);
                    if (source != nil)
                    {
                        CGImageRef cgImage = CGImageSourceCreateImageAtIndex(source, 0, (__bridge CFDictionaryRef)dict);
                        
                        dataImage = [[UIImage alloc] initWithCGImage:cgImage];
                        
                        CGImageRelease(cgImage);
                        CFRelease(source);
                    }
                }
                
                if (dataImage != nil && (availability & TGCacheMemory))
                    [self cacheImage:dataImage withData:nil url:url availability:TGCacheMemory];
            }
        }
        TG_SYNCHRONIZED_END(_dataMemoryCache);
        
        if (dataImage != nil)
            return dataImage;
        
        __block UIImage *diskImageResult = nil;
        dispatch_sync([TGCache diskCacheQueue], ^
        {
            NSDictionary *dict = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:true] forKey:(id)kCGImageSourceShouldCache];
            
            NSURL *realUrl = [[NSURL alloc] initFileURLWithPath:[_diskCachePath stringByAppendingPathComponent:md5String(url)]];
            CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)realUrl, NULL);
            if (source != nil)
            {
                CGImageRef cgImage = CGImageSourceCreateImageAtIndex(source, 0, (__bridge CFDictionaryRef)dict);
                
                UIImage *diskImage = [UIImage imageWithCGImage:cgImage];
                
                if (diskImage != nil && (availability & TGCacheMemory))
                {
                    [self cacheImage:diskImage withData:nil url:url availability:TGCacheMemory];
                }
             
                if (diskImage != nil)
                {
                    diskImageResult = diskImage;
                    
                    if (diskImage != nil)
                    {
                        dispatch_async([TGCache diskCacheQueue], ^
                        {
                            NSData *data = [[NSData alloc] initWithContentsOfURL:realUrl];
                            if (data != nil)
                            {
                                [self cacheCompressedObject:data url:url reentrant:false];
                            }
                        });
                    }
                }
                
                CGImageRelease(cgImage);
                CFRelease(source);
            }
        });
        image = diskImageResult;
        return image;
    }
    
    return nil;
}

- (void)removeFromMemoryCache:(NSString *)url matchEnd:(bool)matchEnd
{
    if (url == nil)
        return;
    
    dispatch_block_t block = ^
    {
        TGCacheRecord *cacheRecord = [_memoryCache objectForKey:url];
        if (cacheRecord != nil)
        {
            self.memoryCacheSize -= (int)cacheRecord.size;
            if (self.memoryCacheSize < 0)
                self.memoryCacheSize = 0;
            [_memoryCache removeObjectForKey:url];
        }
        
        if (matchEnd)
        {
            NSMutableArray *removeKeys = [[NSMutableArray alloc] init];
            
            [_memoryCache enumerateKeysAndObjectsUsingBlock:^(NSString *key, __unused id obj, __unused BOOL *stop)
            {
                if ([key hasSuffix:url])
                    [removeKeys addObject:key];
            }];
            
            [_memoryCache removeObjectsForKeys:removeKeys];
        }
        
        TGCacheRecord *dataCacheRecord = [_dataMemoryCache objectForKey:url];
        if (dataCacheRecord != nil)
        {
            _dataMemoryCacheSize -= dataCacheRecord.size;
            if (_dataMemoryCacheSize < 0)
                _dataMemoryCacheSize = 0;
            [_dataMemoryCache removeObjectForKey:url];
        }
    };
    if ([NSThread isMainThread])
        block();
    else
        dispatch_sync(dispatch_get_main_queue(), block);
}

- (void)cacheThumbnail:(UIImage *)image url:(NSString *)url
{
    if ([self cachedThumbnail:url] != nil)
        return;
    
    if (image != nil)
    {
        int side = 32;
        if (!TGIsRetina())
            side *= 2;
        int size = (int)(side * side * 4);
        if (image.size.width > side || image.size.height > side)
        {
            image = TGScaleImage(image, CGSizeMake(side, side));
        }
        else
            return;
        
        dispatch_block_t block = ^
        {
            TGCacheRecord *cacheRecord = [_thumbnailCache objectForKey:url];
            if (cacheRecord != nil)
            {
                _thumbnailCacheSize -= cacheRecord.size;
                cacheRecord.date = CFAbsoluteTimeGetCurrent();
                cacheRecord.object = image;
                cacheRecord.size = size;
                _thumbnailCacheSize += size;
            }
            else
            {
                [_thumbnailCache setObject:[[TGCacheRecord alloc] initWithObject:image size:size] forKey:url];
                _thumbnailCacheSize += size;
            }
            
            //TGLog(@"Cache thumbnail: %@", url);
            
            //TGLog(@"_thumbnailCacheSize = %d", _thumbnailCacheSize);
            
            if (_thumbnailCacheSize >= _thumbnailMemoryLimit + _thumbnailEvictionInterval)
                [self freeThumbnailCache:_thumbnailMemoryLimit];
        };
        if ([NSThread isMainThread])
            block();
        else
            dispatch_async(dispatch_get_main_queue(), block);
    }
}

- (UIImage *)cachedThumbnail:(NSString *)url
{
    UIImage *image = nil;
    __block UIImage *blockImage = nil;
    dispatch_block_t block = ^
    {
        TGCacheRecord *cacheRecord = [_thumbnailCache objectForKey:url];
        if (cacheRecord != nil)
        {
            cacheRecord.date = CFAbsoluteTimeGetCurrent();
            blockImage = cacheRecord.object;
        }
        else
        {
            //TGLog(@"Thumbnail not found: %@", url);
        }
    };
    if ([NSThread isMainThread])
        block();
    else
        dispatch_async(dispatch_get_main_queue(), block);
    image = blockImage;
    if (image != nil)
        return image;
    
    return nil;
}

- (void)diskCacheContains:(NSString *)url1 orUrl:(NSString *)url2 completion:(void (^)(bool containsFirst, bool containsSecond))completion
{   
    dispatch_async([TGCache diskCacheQueue], ^
    {
        bool cached = [cacheFileManager fileExistsAtPath:[_diskCachePath stringByAppendingPathComponent:md5String(url1)]];
        if (cached)
        {
            if (completion)
                completion(true, false);
        }
        else if (url2 != nil)
        {
            cached = [cacheFileManager fileExistsAtPath:[_diskCachePath stringByAppendingPathComponent:md5String(url2)]];
            if (completion)
                completion(false, cached);
        }
        else
        {
            if (completion)
                completion(false, false);
        }
    });
}

- (bool)diskCacheContainsSync:(NSString *)url
{
    __block bool result = false;
    
    dispatch_sync([TGCache diskCacheQueue], ^
    {
        result = [cacheFileManager fileExistsAtPath:[_diskCachePath stringByAppendingPathComponent:md5String(url)]];
    });
    
    return result;
}

- (void)removeFromDiskCache:(NSString *)url
{
    dispatch_async([TGCache diskCacheQueue], ^
    {
        [cacheFileManager removeItemAtPath:[_diskCachePath stringByAppendingPathComponent:md5String(url)] error:nil];
    });
}

- (NSString *)pathForCachedData:(NSString *)url
{
    return [_diskCachePath stringByAppendingPathComponent:md5String(url)];
}

- (void)changeCacheItemUrl:(NSString *)oldUrl newUrl:(NSString *)newUrl
{
    //TGLog(@"TGCache: rename \"%@\" -> \"%@\"", oldUrl, newUrl);
    dispatch_block_t block = ^
    {
        TGCacheRecord *record = [_memoryCache objectForKey:oldUrl];
        if (record != nil)
        {
            [_memoryCache setObject:record forKey:newUrl];
            [_memoryCache removeObjectForKey:oldUrl];
        }
    };
    if ([NSThread isMainThread])
        block();
    else
        dispatch_async(dispatch_get_main_queue(), block);
    
    dispatch_async([TGCache diskCacheQueue], ^
    {
        NSError *error = nil;
        [cacheFileManager moveItemAtPath:[_diskCachePath stringByAppendingPathComponent:md5String(oldUrl)] toPath:[_diskCachePath stringByAppendingPathComponent:md5String(newUrl)] error:&error];
        if (error != nil)
            TGLog(@"Failed to move: %@", error);
    });
}

- (void)moveToCache:(NSString *)fileUrl cacheUrl:(NSString *)cacheUrl
{
    TGLog(@"TGCache: move \"%@\" -> \"%@\"", fileUrl, cacheUrl);
    dispatch_block_t block = ^
    {
        TGCacheRecord *record = [_memoryCache objectForKey:fileUrl];
        if (record != nil)
        {
            [_memoryCache setObject:record forKey:cacheUrl];
            [_memoryCache removeObjectForKey:fileUrl];
        }
    };
    if ([NSThread isMainThread])
        block();
    else
        dispatch_async(dispatch_get_main_queue(), block);
    
    dispatch_async([TGCache diskCacheQueue], ^
    {
        NSError *error = nil;
        [cacheFileManager moveItemAtPath:fileUrl toPath:[_diskCachePath stringByAppendingPathComponent:md5String(cacheUrl)] error:&error];
        if (error != nil)
            TGLog(@"Failed to move: %@", error);
    });
}

- (void)clearCache:(int)availability
{
    if (availability & TGCacheMemory)
    {
        dispatch_block_t block = ^
        {
            [_memoryCache removeAllObjects];
            self.memoryCacheSize = 0;
        };
        if ([NSThread isMainThread])
            block();
        else
            dispatch_async(dispatch_get_main_queue(), block);
        
        TG_SYNCHRONIZED_BEGIN(_dataMemoryCache);
        [_dataMemoryCache removeAllObjects];
        _dataMemoryCacheSize = 0;
        TG_SYNCHRONIZED_END(_dataMemoryCache);
    }
    if (availability & TGCacheDisk)
    {
        dispatch_async([TGCache diskCacheQueue], ^
        {
            NSDirectoryEnumerator* en = [cacheFileManager enumeratorAtPath:_diskCachePath];
            NSError* error = nil;
            
            int removedCount = 0;
            int failedCount = 0;
            
            NSString* file;
            while (file = [en nextObject])
            {
                if ([cacheFileManager removeItemAtPath:[_diskCachePath stringByAppendingPathComponent:file] error:&error])
                    removedCount++;
                else
                    failedCount++;
            }
            
            TGLog(@"TGCache: removed %d files (%d failed)", removedCount, failedCount);
        });
    }
}

- (NSArray *)storeMemoryCache
{
    NSMutableArray *array = [[NSMutableArray alloc] init];
    
    dispatch_block_t block = ^
    {
        [_memoryCache enumerateKeysAndObjectsUsingBlock:^(NSString *key, __unused id obj, __unused BOOL *stop)
        {
            [array addObject:key];
        }];
    };
    if ([NSThread isMainThread])
        block();
    else
        dispatch_sync(dispatch_get_main_queue(), block);
    
    return array;
}

- (void)restoreMemoryCache:(NSArray *)array
{
    CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
    for (NSString *url in array)
    {
        [self cachedImage:url availability:TGCacheDisk];
    }
    TGLog(@"Cache restored in %f ms", (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0);
}

@end
