/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 "WXStorageModule.h"
#import "WXSDKManager.h"
#import "WXThreadSafeMutableDictionary.h"
#import "WXThreadSafeMutableArray.h"
#import <CommonCrypto/CommonCrypto.h>
#import "WXUtility.h"

static NSString * const WXStorageDirectory            = @"wxstorage";
static NSString * const WXStorageFileName             = @"wxstorage.plist";
static NSString * const WXStorageInfoFileName         = @"wxstorage.info.plist";
static NSString * const WXStorageIndexFileName        = @"wxstorage.index.plist";
static NSUInteger const WXStorageLineLimit            = 1024;
static NSUInteger const WXStorageTotalLimit           = 5 * 1024 * 1024;
static NSString * const WXStorageThreadName           = @"com.taobao.weex.storage";
static NSString * const WXStorageNullValue            = @"#{eulaVlluNegarotSXW}";

@implementation WXStorageModule

@synthesize weexInstance;

WX_EXPORT_METHOD(@selector(length:))
WX_EXPORT_METHOD(@selector(getItem:callback:))
WX_EXPORT_METHOD(@selector(setItem:value:callback:))
WX_EXPORT_METHOD(@selector(setItemPersistent:value:callback:))
WX_EXPORT_METHOD(@selector(getAllKeys:))
WX_EXPORT_METHOD(@selector(removeItem:callback:))

#pragma mark - Export

- (dispatch_queue_t)targetExecuteQueue {
    return [WXStorageModule storageQueue];
}

- (void)length:(WXModuleCallback)callback
{
    if (callback) {
        callback(@{@"result":@"success",@"data":@([[WXStorageModule memory] count])});
    }
}

- (void)getAllKeys:(WXModuleCallback)callback
{
    if (callback) {
        callback(@{@"result":@"success",@"data":[WXStorageModule memory].allKeys});
    }
}

- (void)getItem:(NSString *)key callback:(WXModuleCallback)callback
{
    if ([self checkInput:key]) {
        if (callback) {
            callback(@{@"result":@"failed",@"data":@"key must a string or number!"}); // forgive my english
        }
        return;
    }
    
    if ([key isKindOfClass:[NSNumber class]]) {
        key = [((NSNumber *)key) stringValue]; // oh no!
    }
    
    if ([WXUtility isBlankString:key]) {
        if (callback) {
            callback(@{@"result":@"failed",@"data":@"invalid_param"});
        }
        return ;
    }
    
    NSString *value = [self.memory objectForKey:key];
    if ([WXStorageNullValue isEqualToString:value]) {
        value = [[WXUtility globalCache] objectForKey:key];
        if (!value) {
            NSString *filePath = [WXStorageModule filePathForKey:key];
            NSString *contents = [WXUtility stringWithContentsOfFile:filePath];
            if (contents) {
                [[WXUtility globalCache] setObject:contents forKey:key cost:contents.length];
                value = contents;
            }
        }
    }
    if (!value) {
        [self executeRemoveItem:key];
        if (callback) {
            callback(@{@"result":@"failed",@"data":@"undefined"});
        }
        return;
    }
    [self updateTimestampForKey:key];
    [self updateIndexForKey:key];
    if (callback) {
        callback(@{@"result":@"success",@"data":value});
    }
}

- (void)setItem:(NSString *)key value:(NSString *)value callback:(WXModuleCallback)callback
{
    if ([self checkInput:key]) {
        if (callback) {
            callback(@{@"result":@"failed",@"data":@"key must a string or number!"});
        }
        return;
    }
    if ([self checkInput:value]) {
        if (callback) {
            callback(@{@"result":@"failed",@"data":@"value must a string or number!"});
        }
        return;
    }
    
    if ([key isKindOfClass:[NSNumber class]]) {
        key = [((NSNumber *)key) stringValue];
    }
    
    if ([value isKindOfClass:[NSNumber class]]) {
        value = [((NSNumber *)value) stringValue];
    }
    
    if ([WXUtility isBlankString:key]) {
        if (callback) {
            callback(@{@"result":@"failed",@"data":@"invalid_param"});
        }
        return ;
    }
    [self setObject:value forKey:key persistent:NO callback:callback];
}

- (void)setItemPersistent:(NSString *)key value:(NSString *)value callback:(WXModuleCallback)callback
{
    if ([self checkInput:key]) {
        if (callback) {
            callback(@{@"result":@"failed",@"data":@"key must a string or number!"});
        }
        return;
    }
    if ([self checkInput:value]) {
        if (callback) {
            callback(@{@"result":@"failed",@"data":@"value must a string or number!"});
        }
        return;
    }
    
    if ([key isKindOfClass:[NSNumber class]]) {
        key = [((NSNumber *)key) stringValue];
    }
    
    if ([value isKindOfClass:[NSNumber class]]) {
        value = [((NSNumber *)value) stringValue];
    }
    
    if ([WXUtility isBlankString:key]) {
        if (callback) {
            callback(@{@"result":@"failed",@"data":@"invalid_param"});
        }
        return ;
    }
    [self setObject:value forKey:key persistent:YES callback:callback];
}

- (void)removeItem:(NSString *)key callback:(WXModuleCallback)callback
{
    if ([self checkInput:key]) {
        if (callback) {
            callback(@{@"result":@"failed",@"data":@"key must a string or number!"});
        }
        return;
    }
    
    if ([key isKindOfClass:[NSNumber class]]) {
        key = [((NSNumber *)key) stringValue];
    }
    
    if ([WXUtility isBlankString:key]) {
        if (callback) {
            callback(@{@"result":@"failed",@"data":@"invalid_param"});
        }
        return ;
    }
    BOOL removed = [self executeRemoveItem:key];
    if (removed) {
        if (callback) {
            callback(@{@"result":@"success"});
        }
    } else {
        if (callback) {
            callback(@{@"result":@"failed"});
        }
    }
}

- (BOOL)executeRemoveItem:(NSString *)key {
    if ([WXStorageNullValue isEqualToString:self.memory[key]]) {
        [self.memory removeObjectForKey:key];
        NSDictionary *dict = [self.memory copy];
        [self write:dict toFilePath:[WXStorageModule filePath]];
        dispatch_async([WXStorageModule storageQueue], ^{
            NSString *filePath = [WXStorageModule filePathForKey:key];
            [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
            [[WXUtility globalCache] removeObjectForKey:key];
        });
    } else if (self.memory[key]) {
        [self.memory removeObjectForKey:key];
        NSDictionary *dict = [self.memory copy];
        [self write:dict toFilePath:[WXStorageModule filePath]];
    } else {
        return NO;
    }
    [self removeInfoForKey:key];
    [self removeIndexForKey:key];
    return YES;
}

#pragma mark - Utils
- (void)setObject:(NSString *)obj forKey:(NSString *)key persistent:(BOOL)persistent callback:(WXModuleCallback)callback {
    NSString *filePath = [WXStorageModule filePathForKey:key];
    if (obj.length <= WXStorageLineLimit) {
        if ([WXStorageNullValue isEqualToString:self.memory[key]]) {
            [[WXUtility globalCache] removeObjectForKey:key];
            [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
        }
        self.memory[key] = obj;
        NSDictionary *dict = [self.memory copy];
        [self write:dict toFilePath:[WXStorageModule filePath]];
        [self setInfo:@{@"persistent":@(persistent),@"size":@(obj.length)} ForKey:key];
        [self updateIndexForKey:key];
        [self checkStorageLimit];
        if (callback) {
            callback(@{@"result":@"success"});
        }
        return;
    }
    
    [[WXUtility globalCache] setObject:obj forKey:key cost:obj.length];
    
    if (![WXStorageNullValue isEqualToString:self.memory[key]]) {
        self.memory[key] = WXStorageNullValue;
        NSDictionary *dict = [self.memory copy];
        [self write:dict toFilePath:[WXStorageModule filePath]];
    }
    
    dispatch_async([WXStorageModule storageQueue], ^{
        [obj writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:NULL];
    });
    
    [self setInfo:@{@"persistent":@(persistent),@"size":@(obj.length)} ForKey:key];
    [self updateIndexForKey:key];
    
    [self checkStorageLimit];
    if (callback) {
        callback(@{@"result":@"success"});
    }
}

- (void)checkStorageLimit {
    NSInteger size = [self totalSize] - WXStorageTotalLimit;
    if (size > 0) {
        [self removeItemsBySize:size];
    }
}

- (void)removeItemsBySize:(NSInteger)size {
    NSArray *indexs = [[self indexs] copy];
    if (size < 0 || indexs.count == 0) {
        return;
    }
    
    NSMutableArray *removedKeys = [NSMutableArray array];
    for (NSInteger i = 0; i < indexs.count; i++) {
        NSString *key = indexs[i];
        NSDictionary *info = [self getInfoForKey:key];
        
        // persistent data, can't be removed
        if ([info[@"persistent"] boolValue]) {
            continue;
        }
        
        [removedKeys addObject:key];
        size -= [info[@"size"] integerValue];
        
        if (size < 0) {
            break;
        }
    }
    
    // actually remove data
    for (NSString *key in removedKeys) {
        [self executeRemoveItem:key];
    }
}

- (void)write:(NSDictionary *)dict toFilePath:(NSString *)filePath{
    [dict writeToFile:filePath atomically:YES];
}

+ (NSString *)filePathForKey:(NSString *)key
{
    NSString *safeFileName = [WXUtility md5:key];
    
    return [[WXStorageModule directory] stringByAppendingPathComponent:safeFileName];
}

+ (void)setupDirectory{
    BOOL isDirectory = NO;
    BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:[WXStorageModule directory] isDirectory:&isDirectory];
    if (!isDirectory && !fileExists) {
        [[NSFileManager defaultManager] createDirectoryAtPath:[WXStorageModule directory]
                                  withIntermediateDirectories:YES
                                                   attributes:nil
                                                        error:NULL];
    }
}

+ (NSString *)directory {
    static NSString *storageDirectory = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        storageDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
        storageDirectory = [storageDirectory stringByAppendingPathComponent:WXStorageDirectory];
    });
    return storageDirectory;
}

+ (NSString *)filePath {
    static NSString *storageFilePath = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        storageFilePath = [[WXStorageModule directory] stringByAppendingPathComponent:WXStorageFileName];
    });
    return storageFilePath;
}

+ (NSString *)infoFilePath {
    static NSString *infoFilePath = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        infoFilePath = [[WXStorageModule directory] stringByAppendingPathComponent:WXStorageInfoFileName];
    });
    return infoFilePath;
}

+ (NSString *)indexFilePath {
    static NSString *indexFilePath = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        indexFilePath = [[WXStorageModule directory] stringByAppendingPathComponent:WXStorageIndexFileName];
    });
    return indexFilePath;
}

+ (dispatch_queue_t)storageQueue {
    static dispatch_queue_t storageQueue;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        storageQueue = dispatch_queue_create("com.taobao.weex.storage", DISPATCH_QUEUE_SERIAL);
    });
    return storageQueue;
}

+ (WXThreadSafeMutableDictionary<NSString *, NSString *> *)memory {
    static WXThreadSafeMutableDictionary<NSString *,NSString *> *memory;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [WXStorageModule setupDirectory];
        
        if ([[NSFileManager defaultManager] fileExistsAtPath:[WXStorageModule filePath]]) {
            NSDictionary *contents = [NSDictionary dictionaryWithContentsOfFile:[WXStorageModule filePath]];
            if (contents) {
                memory = [[WXThreadSafeMutableDictionary alloc] initWithDictionary:contents];
            }
        }
        if (!memory) {
            memory = [WXThreadSafeMutableDictionary new];
        }
//        [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification object:nil queue:nil usingBlock:^(__unused NSNotification *note) {
//            [memory removeAllObjects];
//        }];
    });
    return memory;
}

+ (WXThreadSafeMutableDictionary<NSString *, NSDictionary *> *)info {
    static WXThreadSafeMutableDictionary<NSString *,NSDictionary *> *info;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [WXStorageModule setupDirectory];
        
        if ([[NSFileManager defaultManager] fileExistsAtPath:[WXStorageModule infoFilePath]]) {
            NSDictionary *contents = [NSDictionary dictionaryWithContentsOfFile:[WXStorageModule infoFilePath]];
            if (contents) {
                info = [[WXThreadSafeMutableDictionary alloc] initWithDictionary:contents];
            }
        }
        if (!info) {
            info = [WXThreadSafeMutableDictionary new];
        }
    });
    return info;
}

+ (WXThreadSafeMutableArray *)indexs {
    static WXThreadSafeMutableArray *indexs;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [WXStorageModule setupDirectory];
        
        if ([[NSFileManager defaultManager] fileExistsAtPath:[WXStorageModule indexFilePath]]) {
            NSArray *contents = [NSArray arrayWithContentsOfFile:[WXStorageModule indexFilePath]];
            if (contents) {
                indexs = [[WXThreadSafeMutableArray alloc] initWithArray:contents];
            }
        }
        if (!indexs) {
            indexs = [WXThreadSafeMutableArray new];
        }
    });
    return indexs;
}

- (WXThreadSafeMutableDictionary<NSString *, NSString *> *)memory {
    return [WXStorageModule memory];
}

- (WXThreadSafeMutableDictionary<NSString *, NSDictionary *> *)info {
    return [WXStorageModule info];
}

- (WXThreadSafeMutableArray *)indexs {
    return [WXStorageModule indexs];
}

- (BOOL)checkInput:(id)input{
    return !([input isKindOfClass:[NSString class]] || [input isKindOfClass:[NSNumber class]]);
}

#pragma mark
#pragma mark - Storage Info method
- (NSDictionary *)getInfoForKey:(NSString *)key {
    NSDictionary *info = [[self info] objectForKey:key];
    if (!info) {
        return nil;
    }
    return info;
}

- (void)setInfo:(NSDictionary *)info ForKey:(NSString *)key {
    NSAssert(info, @"info must not be nil"); //!OCLint
    
    // save info for key
    NSMutableDictionary *newInfo = [NSMutableDictionary dictionaryWithDictionary:info];
    NSTimeInterval interval = [[NSDate date] timeIntervalSince1970];
    [newInfo setObject:@(interval) forKey:@"ts"];
    
    [[self info] setObject:[newInfo copy] forKey:key];
    NSDictionary *dict = [[self info] copy];
    [self write:dict toFilePath:[WXStorageModule infoFilePath]];
}

- (void)removeInfoForKey:(NSString *)key {
    [[self info] removeObjectForKey:key];
    NSDictionary *dict = [[self info] copy];
    [self write:dict toFilePath:[WXStorageModule infoFilePath]];
}

- (void)updateTimestampForKey:(NSString *)key {
    NSTimeInterval interval = [[NSDate date] timeIntervalSince1970];
    NSDictionary *info = [[self info] objectForKey:key];
    if (!info) {
        info = @{@"persistent":@(NO),@"size":@(0),@"ts":@(interval)};
    } else {
        NSMutableDictionary *newInfo = [NSMutableDictionary dictionaryWithDictionary:info];
        [newInfo setObject:@(interval) forKey:@"ts"];
        info = [newInfo copy];
    }
    
    [[self info] setObject:info forKey:key];
    NSDictionary *dict = [[self info] copy];
    [self write:dict toFilePath:[WXStorageModule infoFilePath]];
}

- (NSInteger)totalSize {
    NSInteger totalSize = 0;
    for (NSDictionary *info in [self info].allValues) {
        totalSize += (info[@"size"] ? [info[@"size"] integerValue] : 0);
    }
    return totalSize;
}

#pragma mark
#pragma mark - Storage Index method
- (void)updateIndexForKey:(NSString *)key {
    [[self indexs] removeObject:key];
    [[self indexs] addObject:key];
    [self write:[[self indexs] copy] toFilePath:[WXStorageModule indexFilePath]];
}

- (void)removeIndexForKey:(NSString *)key {
    [[self indexs] removeObject:key];
    [self write:[[self indexs] copy] toFilePath:[WXStorageModule indexFilePath]];
}

@end

