//
//  ZYJMQTTCoreDataPersistence.m
//  ZYJMQTTClient
//
//  Created by Christoph Krey on 22.03.15.
//  Copyright © 2015-2017 Christoph Krey. All rights reserved.
//

#import "ZYJMQTTCoreDataPersistence.h"
#import "ZYJMQTTLog.h"

@implementation ZYJMQTTFlow
@dynamic clientId;
@dynamic incomingFlag;
@dynamic retainedFlag;
@dynamic commandType;
@dynamic qosLevel;
@dynamic messageId;
@dynamic topic;
@dynamic data;
@dynamic deadline;

@end

@interface ZYJMQTTCoreDataFlow ()

- (ZYJMQTTCoreDataFlow *)initWithContext:(NSManagedObjectContext *)context andObject:(id<ZYJMQTTFlow>)object;
@property NSManagedObjectContext *context;
@property id<ZYJMQTTFlow> object;

@end

@implementation ZYJMQTTCoreDataFlow

@synthesize context;
@synthesize object;

- (ZYJMQTTCoreDataFlow *)initWithContext:(NSManagedObjectContext *)c andObject:(id<ZYJMQTTFlow>)o {
    self = [super init];
    self.context = c;
    self.object = o;
    return self;
}

- (NSString *)clientId {
    __block NSString *_clientId;
    [context performBlockAndWait:^{
        _clientId = self.object.clientId;
    }];
    return _clientId;
}

- (void)setClientId:(NSString *)clientId {
    [context performBlockAndWait:^{
        self.object.clientId = clientId;
    }];
}

- (NSNumber *)incomingFlag {
    __block NSNumber *_incomingFlag;
    [context performBlockAndWait:^{
        _incomingFlag = self.object.incomingFlag;
    }];
    return _incomingFlag;
}

- (void)setIncomingFlag:(NSNumber *)incomingFlag {
    [context performBlockAndWait:^{
        self.object.incomingFlag = incomingFlag;
    }];
}


- (NSNumber *)retainedFlag {
    __block NSNumber *_retainedFlag;
    [context performBlockAndWait:^{
        _retainedFlag = self.object.retainedFlag;
    }];
    return _retainedFlag;
}

- (void)setRetainedFlag:(NSNumber *)retainedFlag {
    [context performBlockAndWait:^{
        self.object.retainedFlag = retainedFlag;
    }];
}

- (NSNumber *)commandType {
    __block NSNumber *_commandType;
    [context performBlockAndWait:^{
        _commandType = self.object.commandType;
    }];
    return _commandType;
}

- (void)setCommandType:(NSNumber *)commandType {
    [context performBlockAndWait:^{
        self.object.commandType = commandType;
    }];
}

- (NSNumber *)qosLevel {
    __block NSNumber *_qosLevel;
    [context performBlockAndWait:^{
        _qosLevel = self.object.qosLevel;
    }];
    return _qosLevel;
}

- (void)setQosLevel:(NSNumber *)qosLevel {
    [context performBlockAndWait:^{
        self.object.qosLevel = qosLevel;
    }];
}

- (NSNumber *)messageId {
    __block NSNumber *_messageId;
    [context performBlockAndWait:^{
        _messageId = self.object.messageId;
    }];
    return _messageId;
}

- (void)setMessageId:(NSNumber *)messageId {
    [context performBlockAndWait:^{
        self.object.messageId = messageId;
    }];
}

- (NSString *)topic {
    __block NSString *_topic;
    [context performBlockAndWait:^{
        _topic = self.object.topic;
    }];
    return _topic;
}

- (void)setTopic:(NSString *)topic {
    [context performBlockAndWait:^{
        self.object.topic = topic;
    }];
}

- (NSData *)data {
    __block NSData *_data;
    [context performBlockAndWait:^{
        _data = self.object.data;
    }];
    return _data;
}

- (void)setData:(NSData *)data {
    [context performBlockAndWait:^{
        self.object.data = data;
    }];
}

- (NSDate *)deadline {
    __block NSDate *_deadline;
    [context performBlockAndWait:^{
        _deadline = self.object.deadline;
    }];
    return _deadline;
}

- (void)setDeadline:(NSDate *)deadline {
    [context performBlockAndWait:^{
        self.object.deadline = deadline;
    }];
}

@end

@interface ZYJMQTTCoreDataPersistence ()

@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (assign, nonatomic) unsigned long long fileSize;
@property (assign, nonatomic) unsigned long long fileSystemFreeSize;

@end

@implementation ZYJMQTTCoreDataPersistence
@synthesize persistent;
@synthesize maxSize;
@synthesize maxMessages;
@synthesize maxWindowSize;

- (ZYJMQTTCoreDataPersistence *)init {
    self = [super init];
    self.persistent = ZYJMQTT_PERSISTENT;
    self.maxSize = ZYJMQTT_MAX_SIZE;
    self.maxMessages = ZYJMQTT_MAX_MESSAGES;
    self.maxWindowSize = ZYJMQTT_MAX_WINDOW_SIZE;
    
    return self;
}

- (NSManagedObjectContext *)managedObjectContext {
    if (!_managedObjectContext) {
        NSPersistentStoreCoordinator *coordinator = [self createPersistentStoreCoordinator];
        _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
       _managedObjectContext.persistentStoreCoordinator = coordinator;
    }
    return _managedObjectContext;
}

- (NSUInteger)windowSize:(NSString *)clientId {
    NSUInteger windowSize = 0;
    NSArray *flows = [self allFlowsforClientId:clientId
                                  incomingFlag:NO];
    for (ZYJMQTTCoreDataFlow *flow in flows) {
        if ((flow.commandType).unsignedIntegerValue != ZYJMQTT_None) {
            windowSize++;
        }
    }
    return windowSize;
}

- (ZYJMQTTCoreDataFlow *)storeMessageForClientId:(NSString *)clientId
                                        topic:(NSString *)topic
                                         data:(NSData *)data
                                   retainFlag:(BOOL)retainFlag
                                          qos:(ZYJMQTTQosLevel)qos
                                        msgId:(UInt16)msgId
                                 incomingFlag:(BOOL)incomingFlag
                                  commandType:(UInt8)commandType
                                     deadline:(NSDate *)deadline {
    if (([self allFlowsforClientId:clientId incomingFlag:incomingFlag].count <= self.maxMessages) &&
        (self.fileSize <= self.maxSize)) {
        ZYJMQTTCoreDataFlow *flow = [self createFlowforClientId:clientId
                                                incomingFlag:incomingFlag
                                                   messageId:msgId];
        flow.topic = topic;
        flow.data = data;
        flow.retainedFlag = @(retainFlag);
        flow.qosLevel = @(qos);
        flow.commandType = [NSNumber numberWithUnsignedInteger:commandType];
        flow.deadline = deadline;
        return flow;
    } else {
        return nil;
    }
}

- (void)deleteFlow:(ZYJMQTTCoreDataFlow *)flow {
    [self.managedObjectContext performBlockAndWait:^{
        [self.managedObjectContext deleteObject:(NSManagedObject *)flow.object];
    }];
    [self sync];
}

- (void)deleteAllFlowsForClientId:(NSString *)clientId {
    NSLog(@"[ZYJMQTTCoreDataPersistence] deleteAllFlowsForClientId %@", clientId);

    [self.managedObjectContext performBlockAndWait:^{
        for (ZYJMQTTCoreDataFlow *flow in [self allFlowsforClientId:clientId incomingFlag:TRUE]) {
            [self.managedObjectContext deleteObject:(NSManagedObject *)flow.object];
        }
        for (ZYJMQTTCoreDataFlow *flow in [self allFlowsforClientId:clientId incomingFlag:FALSE]) {
            [self.managedObjectContext deleteObject:(NSManagedObject *)flow.object];
        }
    }];
    [self sync];
}

- (void)sync {
    [self.managedObjectContext performBlockAndWait:^{
        [self internalSync];
    }];
}

- (void)internalSync {
    if (self.managedObjectContext.hasChanges) {
//        NSLog(@"[ZYJMQTTPersistence] pre-sync: i%lu u%lu d%lu",
//                     (unsigned long)self.managedObjectContext.insertedObjects.count,
//                     (unsigned long)self.managedObjectContext.updatedObjects.count,
//                     (unsigned long)self.managedObjectContext.deletedObjects.count
//                     );
        NSError *error = nil;
        if (![self.managedObjectContext save:&error]) {
            NSLog(@"[ZYJMQTTPersistence] sync error %@", error);
        }
        if (self.managedObjectContext.hasChanges) {
            NSLog(@"[ZYJMQTTPersistence] sync not complete");
        }
//        NSLog(@"[ZYJMQTTPersistence] postsync: i%lu u%lu d%lu",
//                     (unsigned long)self.managedObjectContext.insertedObjects.count,
//                     (unsigned long)self.managedObjectContext.updatedObjects.count,
//                     (unsigned long)self.managedObjectContext.deletedObjects.count
//                     );
        [self sizes];
    }
}

- (NSArray *)allFlowsforClientId:(NSString *)clientId
                    incomingFlag:(BOOL)incomingFlag {
    NSMutableArray *flows = [NSMutableArray array];
    __block NSArray *rows;
    [self.managedObjectContext performBlockAndWait:^{
        
        NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"ZYJMQTTFlow"];
        fetchRequest.predicate = [NSPredicate predicateWithFormat:
                                  @"clientId = %@ and incomingFlag = %@",
                                  clientId,
                                  @(incomingFlag)
                                  ];
        fetchRequest.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"deadline" ascending:YES]];
        NSError *error = nil;
        rows = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
        if (!rows) {
//            NSLog(@"[ZYJMQTTPersistence] allFlowsforClientId %@", error);
        }
    }];
    for (id<ZYJMQTTFlow>row in rows) {
        [flows addObject:[[ZYJMQTTCoreDataFlow alloc] initWithContext:self.managedObjectContext andObject:row]];
    }
    return flows;
}

- (ZYJMQTTCoreDataFlow *)flowforClientId:(NSString *)clientId
                         incomingFlag:(BOOL)incomingFlag
                            messageId:(UInt16)messageId {
    __block ZYJMQTTCoreDataFlow *flow = nil;

//    NSLog(@"flowforClientId requestingPerform");
    [self.managedObjectContext performBlockAndWait:^{
        flow = [self internalFlowForClientId:clientId
                                incomingFlag:incomingFlag
                                   messageId:messageId];
    }];
//    NSLog(@"flowforClientId performed");
    return flow;
}

- (ZYJMQTTCoreDataFlow *)internalFlowForClientId:(NSString *)clientId
                                 incomingFlag:(BOOL)incomingFlag
                                    messageId:(UInt16)messageId {
    ZYJMQTTCoreDataFlow *flow = nil;

//    NSLog(@"flowforClientId performing");

    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"ZYJMQTTFlow"];
    fetchRequest.predicate = [NSPredicate predicateWithFormat:
                              @"clientId = %@ and incomingFlag = %@ and messageId = %@",
                              clientId,
                              @(incomingFlag),
                              @(messageId)
                              ];
    NSArray *rows;
    NSError *error = nil;
    rows = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
    if (!rows) {
//        NSLog(@"[ZYJMQTTPersistence] flowForClientId %@", error);
    } else {
        if (rows.count) {
            flow = [[ZYJMQTTCoreDataFlow alloc] initWithContext:self.managedObjectContext andObject:rows.lastObject];
        }
    }
    return flow;
}

- (ZYJMQTTCoreDataFlow *)createFlowforClientId:(NSString *)clientId
                               incomingFlag:(BOOL)incomingFlag
                                  messageId:(UInt16)messageId {
    ZYJMQTTCoreDataFlow *flow = (ZYJMQTTCoreDataFlow *)[self flowforClientId:clientId
                                                          incomingFlag:incomingFlag
                                                             messageId:messageId];
    if (!flow) {
        __block id<ZYJMQTTFlow> row;
        [self.managedObjectContext performBlockAndWait:^{
            row = [NSEntityDescription insertNewObjectForEntityForName:@"ZYJMQTTFlow"
                                                inManagedObjectContext:self.managedObjectContext];
            
            row.clientId = clientId;
            row.incomingFlag = @(incomingFlag);
            row.messageId = @(messageId);
        }];
        flow = [[ZYJMQTTCoreDataFlow alloc] initWithContext:self.managedObjectContext andObject:row];
    }

    return flow;
}

#pragma mark - Core Data stack

- (NSManagedObjectModel *)createManagedObjectModel {
    NSManagedObjectModel *managedObjectModel = [[NSManagedObjectModel alloc] init];
    NSMutableArray *entities = [[NSMutableArray alloc] init];
    NSMutableArray *properties = [[NSMutableArray alloc] init];
    
    NSAttributeDescription *attributeDescription;
    
    attributeDescription = [[NSAttributeDescription alloc] init];
    attributeDescription.name = @"clientId";
    attributeDescription.attributeType = NSStringAttributeType;
    attributeDescription.attributeValueClassName = @"NSString";
    [properties addObject:attributeDescription];
    
    attributeDescription = [[NSAttributeDescription alloc] init];
    attributeDescription.name = @"incomingFlag";
    attributeDescription.attributeType = NSBooleanAttributeType;
    attributeDescription.attributeValueClassName = @"NSNumber";
    [properties addObject:attributeDescription];
    
    attributeDescription = [[NSAttributeDescription alloc] init];
    attributeDescription.name = @"retainedFlag";
    attributeDescription.attributeType = NSBooleanAttributeType;
    attributeDescription.attributeValueClassName = @"NSNumber";
    [properties addObject:attributeDescription];
    
    attributeDescription = [[NSAttributeDescription alloc] init];
    attributeDescription.name = @"commandType";
    attributeDescription.attributeType = NSInteger16AttributeType;
    attributeDescription.attributeValueClassName = @"NSNumber";
    [properties addObject:attributeDescription];
    
    attributeDescription = [[NSAttributeDescription alloc] init];
    attributeDescription.name = @"qosLevel";
    attributeDescription.attributeType = NSInteger16AttributeType;
    attributeDescription.attributeValueClassName = @"NSNumber";
    [properties addObject:attributeDescription];
    
    attributeDescription = [[NSAttributeDescription alloc] init];
    attributeDescription.name = @"messageId";
    attributeDescription.attributeType = NSInteger32AttributeType;
    attributeDescription.attributeValueClassName = @"NSNumber";
    [properties addObject:attributeDescription];
    
    attributeDescription = [[NSAttributeDescription alloc] init];
    attributeDescription.name = @"topic";
    attributeDescription.attributeType = NSStringAttributeType;
    attributeDescription.attributeValueClassName = @"NSString";
    [properties addObject:attributeDescription];
    
    attributeDescription = [[NSAttributeDescription alloc] init];
    attributeDescription.name = @"data";
    attributeDescription.attributeType = NSBinaryDataAttributeType;
    attributeDescription.attributeValueClassName = @"NSData";
    [properties addObject:attributeDescription];
    
    attributeDescription = [[NSAttributeDescription alloc] init];
    attributeDescription.name = @"deadline";
    attributeDescription.attributeType = NSDateAttributeType;
    attributeDescription.attributeValueClassName = @"NSDate";
    [properties addObject:attributeDescription];
    
    NSEntityDescription *entityDescription = [[NSEntityDescription alloc] init];
    entityDescription.name = @"ZYJMQTTFlow";
    entityDescription.managedObjectClassName = @"ZYJMQTTFlow";
    entityDescription.abstract = FALSE;
    entityDescription.properties = properties;
    
    [entities addObject:entityDescription];
    managedObjectModel.entities = entities;
    
    return managedObjectModel;
}

- (NSPersistentStoreCoordinator *)createPersistentStoreCoordinator {
    NSURL *persistentStoreURL = [[self applicationDocumentsDirectory]
                                 URLByAppendingPathComponent:@"ZYJMQTTClient"];
    NSLog(@"[ZYJMQTTPersistence] Persistent store: %@", persistentStoreURL.path);
    
    
    NSError *error = nil;
    NSManagedObjectModel *model = [self createManagedObjectModel];
    NSPersistentStoreCoordinator *persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc]
                                  initWithManagedObjectModel:model];
    NSDictionary *options = @{NSMigratePersistentStoresAutomaticallyOption: @YES,
                              NSInferMappingModelAutomaticallyOption: @YES,
                              NSSQLiteAnalyzeOption: @YES,
                              NSSQLiteManualVacuumOption: @YES
                              };
    
    if (![persistentStoreCoordinator addPersistentStoreWithType:self.persistent ? NSSQLiteStoreType : NSInMemoryStoreType
                                                  configuration:nil
                                                            URL:self.persistent ? persistentStoreURL : nil
                                                        options:options
                                                          error:&error]) {
        NSLog(@"[ZYJMQTTPersistence] managedObjectContext save: %@", error);
        persistentStoreCoordinator = nil;
    }
    return persistentStoreCoordinator;
}

#pragma mark - Application's Documents directory

- (NSURL *)applicationDocumentsDirectory
{
    return [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].lastObject;
}

- (void)sizes {
    if (self.persistent) {
        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
        NSString *documentsDirectory = paths[0];
        NSString *persistentStorePath = [documentsDirectory stringByAppendingPathComponent:@"ZYJMQTTClient"];

        NSError *error = nil;
        NSDictionary *fileAttributes = [[NSFileManager defaultManager]
                                        attributesOfItemAtPath:persistentStorePath error:&error];
        NSDictionary *fileSystemAttributes = [[NSFileManager defaultManager]
                                              attributesOfFileSystemForPath:persistentStorePath
                                              error:&error];
        self.fileSize = [fileAttributes[NSFileSize] unsignedLongLongValue];
        self.fileSystemFreeSize = [fileSystemAttributes[NSFileSystemFreeSize] unsignedLongLongValue];
    } else {
        self.fileSize = 0;
        self.fileSystemFreeSize = 0;
    }
//    NSLog(@"[ZYJMQTTPersistence] sizes %llu/%llu", self.fileSize, self.fileSystemFreeSize);
}
@end
