/*
 * This file is part of the YSSDWebImage package.
 * (c) Olivier Poitrey <rs@dailymotion.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

#import "YSSDImageCachesManager.h"
#import "YSSDImageCachesManagerOperation.h"
#import "YSSDImageCache.h"
#import "YSSDInternalMacros.h"

@interface YSSDImageCachesManager ()

@property (nonatomic, strong, nonnull) dispatch_semaphore_t cachesLock;

@end

@implementation YSSDImageCachesManager
{
    NSMutableArray<id<YSSDImageCache>> *_imageCaches;
}

+ (YSSDImageCachesManager *)sharedManager {
    static dispatch_once_t onceToken;
    static YSSDImageCachesManager *manager;
    dispatch_once(&onceToken, ^{
        manager = [[YSSDImageCachesManager alloc] init];
    });
    return manager;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        self.queryOperationPolicy = YSSDImageCachesManagerOperationPolicySerial;
        self.storeOperationPolicy = YSSDImageCachesManagerOperationPolicyHighestOnly;
        self.removeOperationPolicy = YSSDImageCachesManagerOperationPolicyConcurrent;
        self.containsOperationPolicy = YSSDImageCachesManagerOperationPolicySerial;
        self.clearOperationPolicy = YSSDImageCachesManagerOperationPolicyConcurrent;
        // initialize with default image caches
        _imageCaches = [NSMutableArray arrayWithObject:[YSSDImageCache sharedImageCache]];
        _cachesLock = dispatch_semaphore_create(1);
    }
    return self;
}

- (NSArray<id<YSSDImageCache>> *)caches {
    YSSD_LOCK(self.cachesLock);
    NSArray<id<YSSDImageCache>> *caches = [_imageCaches copy];
    YSSD_UNLOCK(self.cachesLock);
    return caches;
}

- (void)setCaches:(NSArray<id<YSSDImageCache>> *)caches {
    YSSD_LOCK(self.cachesLock);
    [_imageCaches removeAllObjects];
    if (caches.count) {
        [_imageCaches addObjectsFromArray:caches];
    }
    YSSD_UNLOCK(self.cachesLock);
}

#pragma mark - Cache IO operations

- (void)addCache:(id<YSSDImageCache>)cache {
    if (![cache conformsToProtocol:@protocol(YSSDImageCache)]) {
        return;
    }
    YSSD_LOCK(self.cachesLock);
    [_imageCaches addObject:cache];
    YSSD_UNLOCK(self.cachesLock);
}

- (void)removeCache:(id<YSSDImageCache>)cache {
    if (![cache conformsToProtocol:@protocol(YSSDImageCache)]) {
        return;
    }
    YSSD_LOCK(self.cachesLock);
    [_imageCaches removeObject:cache];
    YSSD_UNLOCK(self.cachesLock);
}

#pragma mark - YSSDImageCache

- (id<YSSDWebImageOperation>)queryImageForKey:(NSString *)key options:(YSSDWebImageOptions)options context:(YSSDWebImageContext *)context completion:(YSSDImageCacheQueryCompletionBlock)completionBlock {
    return [self queryImageForKey:key options:options context:context cacheType:YSSDImageCacheTypeAll completion:completionBlock];
}

- (id<YSSDWebImageOperation>)queryImageForKey:(NSString *)key options:(YSSDWebImageOptions)options context:(YSSDWebImageContext *)context cacheType:(YSSDImageCacheType)cacheType completion:(YSSDImageCacheQueryCompletionBlock)completionBlock {
    if (!key) {
        return nil;
    }
    NSArray<id<YSSDImageCache>> *caches = self.caches;
    NSUInteger count = caches.count;
    if (count == 0) {
        return nil;
    } else if (count == 1) {
        return [caches.firstObject queryImageForKey:key options:options context:context cacheType:cacheType completion:completionBlock];
    }
    switch (self.queryOperationPolicy) {
        case YSSDImageCachesManagerOperationPolicyHighestOnly: {
            id<YSSDImageCache> cache = caches.lastObject;
            return [cache queryImageForKey:key options:options context:context cacheType:cacheType completion:completionBlock];
        }
            break;
        case YSSDImageCachesManagerOperationPolicyLowestOnly: {
            id<YSSDImageCache> cache = caches.firstObject;
            return [cache queryImageForKey:key options:options context:context cacheType:cacheType completion:completionBlock];
        }
            break;
        case YSSDImageCachesManagerOperationPolicyConcurrent: {
            YSSDImageCachesManagerOperation *operation = [YSSDImageCachesManagerOperation new];
            [operation beginWithTotalCount:caches.count];
            [self concurrentQueryImageForKey:key options:options context:context cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation];
            return operation;
        }
            break;
        case YSSDImageCachesManagerOperationPolicySerial: {
            YSSDImageCachesManagerOperation *operation = [YSSDImageCachesManagerOperation new];
            [operation beginWithTotalCount:caches.count];
            [self serialQueryImageForKey:key options:options context:context cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation];
            return operation;
        }
            break;
        default:
            return nil;
            break;
    }
}

- (void)storeImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key cacheType:(YSSDImageCacheType)cacheType completion:(YSSDWebImageNoParamsBlock)completionBlock {
    if (!key) {
        return;
    }
    NSArray<id<YSSDImageCache>> *caches = self.caches;
    NSUInteger count = caches.count;
    if (count == 0) {
        return;
    } else if (count == 1) {
        [caches.firstObject storeImage:image imageData:imageData forKey:key cacheType:cacheType completion:completionBlock];
        return;
    }
    switch (self.storeOperationPolicy) {
        case YSSDImageCachesManagerOperationPolicyHighestOnly: {
            id<YSSDImageCache> cache = caches.lastObject;
            [cache storeImage:image imageData:imageData forKey:key cacheType:cacheType completion:completionBlock];
        }
            break;
        case YSSDImageCachesManagerOperationPolicyLowestOnly: {
            id<YSSDImageCache> cache = caches.firstObject;
            [cache storeImage:image imageData:imageData forKey:key cacheType:cacheType completion:completionBlock];
        }
            break;
        case YSSDImageCachesManagerOperationPolicyConcurrent: {
            YSSDImageCachesManagerOperation *operation = [YSSDImageCachesManagerOperation new];
            [operation beginWithTotalCount:caches.count];
            [self concurrentStoreImage:image imageData:imageData forKey:key cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation];
        }
            break;
        case YSSDImageCachesManagerOperationPolicySerial: {
            [self serialStoreImage:image imageData:imageData forKey:key cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator];
        }
            break;
        default:
            break;
    }
}

- (void)removeImageForKey:(NSString *)key cacheType:(YSSDImageCacheType)cacheType completion:(YSSDWebImageNoParamsBlock)completionBlock {
    if (!key) {
        return;
    }
    NSArray<id<YSSDImageCache>> *caches = self.caches;
    NSUInteger count = caches.count;
    if (count == 0) {
        return;
    } else if (count == 1) {
        [caches.firstObject removeImageForKey:key cacheType:cacheType completion:completionBlock];
        return;
    }
    switch (self.removeOperationPolicy) {
        case YSSDImageCachesManagerOperationPolicyHighestOnly: {
            id<YSSDImageCache> cache = caches.lastObject;
            [cache removeImageForKey:key cacheType:cacheType completion:completionBlock];
        }
            break;
        case YSSDImageCachesManagerOperationPolicyLowestOnly: {
            id<YSSDImageCache> cache = caches.firstObject;
            [cache removeImageForKey:key cacheType:cacheType completion:completionBlock];
        }
            break;
        case YSSDImageCachesManagerOperationPolicyConcurrent: {
            YSSDImageCachesManagerOperation *operation = [YSSDImageCachesManagerOperation new];
            [operation beginWithTotalCount:caches.count];
            [self concurrentRemoveImageForKey:key cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation];
        }
            break;
        case YSSDImageCachesManagerOperationPolicySerial: {
            [self serialRemoveImageForKey:key cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator];
        }
            break;
        default:
            break;
    }
}

- (void)containsImageForKey:(NSString *)key cacheType:(YSSDImageCacheType)cacheType completion:(YSSDImageCacheContainsCompletionBlock)completionBlock {
    if (!key) {
        return;
    }
    NSArray<id<YSSDImageCache>> *caches = self.caches;
    NSUInteger count = caches.count;
    if (count == 0) {
        return;
    } else if (count == 1) {
        [caches.firstObject containsImageForKey:key cacheType:cacheType completion:completionBlock];
        return;
    }
    switch (self.clearOperationPolicy) {
        case YSSDImageCachesManagerOperationPolicyHighestOnly: {
            id<YSSDImageCache> cache = caches.lastObject;
            [cache containsImageForKey:key cacheType:cacheType completion:completionBlock];
        }
            break;
        case YSSDImageCachesManagerOperationPolicyLowestOnly: {
            id<YSSDImageCache> cache = caches.firstObject;
            [cache containsImageForKey:key cacheType:cacheType completion:completionBlock];
        }
            break;
        case YSSDImageCachesManagerOperationPolicyConcurrent: {
            YSSDImageCachesManagerOperation *operation = [YSSDImageCachesManagerOperation new];
            [operation beginWithTotalCount:caches.count];
            [self concurrentContainsImageForKey:key cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation];
        }
            break;
        case YSSDImageCachesManagerOperationPolicySerial: {
            YSSDImageCachesManagerOperation *operation = [YSSDImageCachesManagerOperation new];
            [operation beginWithTotalCount:caches.count];
            [self serialContainsImageForKey:key cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation];
        }
            break;
        default:
            break;
    }
}

- (void)clearWithCacheType:(YSSDImageCacheType)cacheType completion:(YSSDWebImageNoParamsBlock)completionBlock {
    NSArray<id<YSSDImageCache>> *caches = self.caches;
    NSUInteger count = caches.count;
    if (count == 0) {
        return;
    } else if (count == 1) {
        [caches.firstObject clearWithCacheType:cacheType completion:completionBlock];
        return;
    }
    switch (self.clearOperationPolicy) {
        case YSSDImageCachesManagerOperationPolicyHighestOnly: {
            id<YSSDImageCache> cache = caches.lastObject;
            [cache clearWithCacheType:cacheType completion:completionBlock];
        }
            break;
        case YSSDImageCachesManagerOperationPolicyLowestOnly: {
            id<YSSDImageCache> cache = caches.firstObject;
            [cache clearWithCacheType:cacheType completion:completionBlock];
        }
            break;
        case YSSDImageCachesManagerOperationPolicyConcurrent: {
            YSSDImageCachesManagerOperation *operation = [YSSDImageCachesManagerOperation new];
            [operation beginWithTotalCount:caches.count];
            [self concurrentClearWithCacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation];
        }
            break;
        case YSSDImageCachesManagerOperationPolicySerial: {
            [self serialClearWithCacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator];
        }
            break;
        default:
            break;
    }
}

#pragma mark - Concurrent Operation

- (void)concurrentQueryImageForKey:(NSString *)key options:(YSSDWebImageOptions)options context:(YSSDWebImageContext *)context cacheType:(YSSDImageCacheType)queryCacheType completion:(YSSDImageCacheQueryCompletionBlock)completionBlock enumerator:(NSEnumerator<id<YSSDImageCache>> *)enumerator operation:(YSSDImageCachesManagerOperation *)operation {
    NSParameterAssert(enumerator);
    NSParameterAssert(operation);
    for (id<YSSDImageCache> cache in enumerator) {
        [cache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable image, NSData * _Nullable data, YSSDImageCacheType cacheType) {
            if (operation.isCancelled) {
                // Cancelled
                return;
            }
            if (operation.isFinished) {
                // Finished
                return;
            }
            [operation completeOne];
            if (image) {
                // Success
                [operation done];
                if (completionBlock) {
                    completionBlock(image, data, cacheType);
                }
                return;
            }
            if (operation.pendingCount == 0) {
                // Complete
                [operation done];
                if (completionBlock) {
                    completionBlock(nil, nil, YSSDImageCacheTypeNone);
                }
            }
        }];
    }
}

- (void)concurrentStoreImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key cacheType:(YSSDImageCacheType)cacheType completion:(YSSDWebImageNoParamsBlock)completionBlock enumerator:(NSEnumerator<id<YSSDImageCache>> *)enumerator operation:(YSSDImageCachesManagerOperation *)operation {
    NSParameterAssert(enumerator);
    NSParameterAssert(operation);
    for (id<YSSDImageCache> cache in enumerator) {
        [cache storeImage:image imageData:imageData forKey:key cacheType:cacheType completion:^{
            if (operation.isCancelled) {
                // Cancelled
                return;
            }
            if (operation.isFinished) {
                // Finished
                return;
            }
            [operation completeOne];
            if (operation.pendingCount == 0) {
                // Complete
                [operation done];
                if (completionBlock) {
                    completionBlock();
                }
            }
        }];
    }
}

- (void)concurrentRemoveImageForKey:(NSString *)key cacheType:(YSSDImageCacheType)cacheType completion:(YSSDWebImageNoParamsBlock)completionBlock enumerator:(NSEnumerator<id<YSSDImageCache>> *)enumerator operation:(YSSDImageCachesManagerOperation *)operation {
    NSParameterAssert(enumerator);
    NSParameterAssert(operation);
    for (id<YSSDImageCache> cache in enumerator) {
        [cache removeImageForKey:key cacheType:cacheType completion:^{
            if (operation.isCancelled) {
                // Cancelled
                return;
            }
            if (operation.isFinished) {
                // Finished
                return;
            }
            [operation completeOne];
            if (operation.pendingCount == 0) {
                // Complete
                [operation done];
                if (completionBlock) {
                    completionBlock();
                }
            }
        }];
    }
}

- (void)concurrentContainsImageForKey:(NSString *)key cacheType:(YSSDImageCacheType)cacheType completion:(YSSDImageCacheContainsCompletionBlock)completionBlock enumerator:(NSEnumerator<id<YSSDImageCache>> *)enumerator operation:(YSSDImageCachesManagerOperation *)operation {
    NSParameterAssert(enumerator);
    NSParameterAssert(operation);
    for (id<YSSDImageCache> cache in enumerator) {
        [cache containsImageForKey:key cacheType:cacheType completion:^(YSSDImageCacheType containsCacheType) {
            if (operation.isCancelled) {
                // Cancelled
                return;
            }
            if (operation.isFinished) {
                // Finished
                return;
            }
            [operation completeOne];
            if (containsCacheType != YSSDImageCacheTypeNone) {
                // Success
                [operation done];
                if (completionBlock) {
                    completionBlock(containsCacheType);
                }
                return;
            }
            if (operation.pendingCount == 0) {
                // Complete
                [operation done];
                if (completionBlock) {
                    completionBlock(YSSDImageCacheTypeNone);
                }
            }
        }];
    }
}

- (void)concurrentClearWithCacheType:(YSSDImageCacheType)cacheType completion:(YSSDWebImageNoParamsBlock)completionBlock enumerator:(NSEnumerator<id<YSSDImageCache>> *)enumerator operation:(YSSDImageCachesManagerOperation *)operation {
    NSParameterAssert(enumerator);
    NSParameterAssert(operation);
    for (id<YSSDImageCache> cache in enumerator) {
        [cache clearWithCacheType:cacheType completion:^{
            if (operation.isCancelled) {
                // Cancelled
                return;
            }
            if (operation.isFinished) {
                // Finished
                return;
            }
            [operation completeOne];
            if (operation.pendingCount == 0) {
                // Complete
                [operation done];
                if (completionBlock) {
                    completionBlock();
                }
            }
        }];
    }
}

#pragma mark - Serial Operation

- (void)serialQueryImageForKey:(NSString *)key options:(YSSDWebImageOptions)options context:(YSSDWebImageContext *)context cacheType:(YSSDImageCacheType)queryCacheType completion:(YSSDImageCacheQueryCompletionBlock)completionBlock enumerator:(NSEnumerator<id<YSSDImageCache>> *)enumerator operation:(YSSDImageCachesManagerOperation *)operation {
    NSParameterAssert(enumerator);
    NSParameterAssert(operation);
    id<YSSDImageCache> cache = enumerator.nextObject;
    if (!cache) {
        // Complete
        [operation done];
        if (completionBlock) {
            completionBlock(nil, nil, YSSDImageCacheTypeNone);
        }
        return;
    }
    @WeakObj(self);
    [cache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable image, NSData * _Nullable data, YSSDImageCacheType cacheType) {
        @StrongObj(self);
        if (operation.isCancelled) {
            // Cancelled
            return;
        }
        if (operation.isFinished) {
            // Finished
            return;
        }
        [operation completeOne];
        if (image) {
            // Success
            [operation done];
            if (completionBlock) {
                completionBlock(image, data, cacheType);
            }
            return;
        }
        // Next
        [self serialQueryImageForKey:key options:options context:context cacheType:queryCacheType completion:completionBlock enumerator:enumerator operation:operation];
    }];
}

- (void)serialStoreImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key cacheType:(YSSDImageCacheType)cacheType completion:(YSSDWebImageNoParamsBlock)completionBlock enumerator:(NSEnumerator<id<YSSDImageCache>> *)enumerator {
    NSParameterAssert(enumerator);
    id<YSSDImageCache> cache = enumerator.nextObject;
    if (!cache) {
        // Complete
        if (completionBlock) {
            completionBlock();
        }
        return;
    }
    @WeakObj(self);
    [cache storeImage:image imageData:imageData forKey:key cacheType:cacheType completion:^{
        @StrongObj(self);
        // Next
        [self serialStoreImage:image imageData:imageData forKey:key cacheType:cacheType completion:completionBlock enumerator:enumerator];
    }];
}

- (void)serialRemoveImageForKey:(NSString *)key cacheType:(YSSDImageCacheType)cacheType completion:(YSSDWebImageNoParamsBlock)completionBlock enumerator:(NSEnumerator<id<YSSDImageCache>> *)enumerator {
    NSParameterAssert(enumerator);
    id<YSSDImageCache> cache = enumerator.nextObject;
    if (!cache) {
        // Complete
        if (completionBlock) {
            completionBlock();
        }
        return;
    }
    @WeakObj(self);
    [cache removeImageForKey:key cacheType:cacheType completion:^{
        @StrongObj(self);
        // Next
        [self serialRemoveImageForKey:key cacheType:cacheType completion:completionBlock enumerator:enumerator];
    }];
}

- (void)serialContainsImageForKey:(NSString *)key cacheType:(YSSDImageCacheType)cacheType completion:(YSSDImageCacheContainsCompletionBlock)completionBlock enumerator:(NSEnumerator<id<YSSDImageCache>> *)enumerator operation:(YSSDImageCachesManagerOperation *)operation {
    NSParameterAssert(enumerator);
    NSParameterAssert(operation);
    id<YSSDImageCache> cache = enumerator.nextObject;
    if (!cache) {
        // Complete
        [operation done];
        if (completionBlock) {
            completionBlock(YSSDImageCacheTypeNone);
        }
        return;
    }
    @WeakObj(self);
    [cache containsImageForKey:key cacheType:cacheType completion:^(YSSDImageCacheType containsCacheType) {
        @StrongObj(self);
        if (operation.isCancelled) {
            // Cancelled
            return;
        }
        if (operation.isFinished) {
            // Finished
            return;
        }
        [operation completeOne];
        if (containsCacheType != YSSDImageCacheTypeNone) {
            // Success
            [operation done];
            if (completionBlock) {
                completionBlock(containsCacheType);
            }
            return;
        }
        // Next
        [self serialContainsImageForKey:key cacheType:cacheType completion:completionBlock enumerator:enumerator operation:operation];
    }];
}

- (void)serialClearWithCacheType:(YSSDImageCacheType)cacheType completion:(YSSDWebImageNoParamsBlock)completionBlock enumerator:(NSEnumerator<id<YSSDImageCache>> *)enumerator {
    NSParameterAssert(enumerator);
    id<YSSDImageCache> cache = enumerator.nextObject;
    if (!cache) {
        // Complete
        if (completionBlock) {
            completionBlock();
        }
        return;
    }
    @WeakObj(self);
    [cache clearWithCacheType:cacheType completion:^{
        @StrongObj(self);
        // Next
        [self serialClearWithCacheType:cacheType completion:completionBlock enumerator:enumerator];
    }];
}

@end
