#import "DDAbstractDatabaseLogger.h"
#import <math.h>

/**
 * Welcome to Cocoa Lumberjack!
 * 
 * The project page has a wealth of documentation if you have any questions.
 * https://github.com/CocoaLumberjack/CocoaLumberjack
 * 
 * If you're new to the project you may wish to read the "Getting Started" wiki.
 * https://github.com/CocoaLumberjack/CocoaLumberjack/wiki/GettingStarted
**/

#if ! __has_feature(objc_arc)
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif

@interface DDAbstractDatabaseLogger ()
- (void)destroySaveTimer;
- (void)destroyDeleteTimer;
@end

#pragma mark -

@implementation DDAbstractDatabaseLogger

- (id)init
{
    if ((self = [super init]))
    {
        saveThreshold = 500;
        saveInterval = 60;           // 60 seconds
        maxAge = (60 * 60 * 24 * 7); //  7 days
        deleteInterval = (60 * 5);   //  5 minutes
    }
    return self;
}

- (void)dealloc
{
    [self destroySaveTimer];
    [self destroyDeleteTimer];
    
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Override Me
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (BOOL)db_log:(DDLogMessage *)logMessage
{
    // Override me and add your implementation.
    // 
    // Return YES if an item was added to the buffer.
    // Return NO if the logMessage was ignored.
    
    return NO;
}

- (void)db_save
{
    // Override me and add your implementation.
}

- (void)db_delete
{
    // Override me and add your implementation.
}

- (void)db_saveAndDelete
{
    // Override me and add your implementation.
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Private API
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (void)performSaveAndSuspendSaveTimer
{
    if (unsavedCount > 0)
    {
        if (deleteOnEverySave)
            [self db_saveAndDelete];
        else
            [self db_save];
    }
    
    unsavedCount = 0;
    unsavedTime = 0;
    
    if (saveTimer && !saveTimerSuspended)
    {
        dispatch_suspend(saveTimer);
        saveTimerSuspended = YES;
    }
}

- (void)performDelete
{
    if (maxAge > 0.0)
    {
        [self db_delete];
        
        lastDeleteTime = dispatch_time(DISPATCH_TIME_NOW, 0);
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Timers
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (void)destroySaveTimer
{
    if (saveTimer)
    {
        dispatch_source_cancel(saveTimer);
        if (saveTimerSuspended)
        {
            // Must resume a timer before releasing it (or it will crash)
            dispatch_resume(saveTimer);
            saveTimerSuspended = NO;
        }
        #if !OS_OBJECT_USE_OBJC
        dispatch_release(saveTimer);
        #endif
        saveTimer = NULL;
    }
}

- (void)updateAndResumeSaveTimer
{
    if ((saveTimer != NULL) && (saveInterval > 0.0) && (unsavedTime > 0.0))
    {
        uint64_t interval = (uint64_t)(saveInterval * NSEC_PER_SEC);
        dispatch_time_t startTime = dispatch_time(unsavedTime, interval);
        
        dispatch_source_set_timer(saveTimer, startTime, interval, 1.0);
        
        if (saveTimerSuspended)
        {
            dispatch_resume(saveTimer);
            saveTimerSuspended = NO;
        }
    }
}

- (void)createSuspendedSaveTimer
{
    if ((saveTimer == NULL) && (saveInterval > 0.0))
    {
        saveTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, loggerQueue);
        
        dispatch_source_set_event_handler(saveTimer, ^{ @autoreleasepool {
            
            [self performSaveAndSuspendSaveTimer];
            
        }});
        
        saveTimerSuspended = YES;
    }
}

- (void)destroyDeleteTimer
{
    if (deleteTimer)
    {
        dispatch_source_cancel(deleteTimer);
        #if !OS_OBJECT_USE_OBJC
        dispatch_release(deleteTimer);
        #endif
        deleteTimer = NULL;
    }
}

- (void)updateDeleteTimer
{
    if ((deleteTimer != NULL) && (deleteInterval > 0.0) && (maxAge > 0.0))
    {
        uint64_t interval = (uint64_t)(deleteInterval * NSEC_PER_SEC);
        dispatch_time_t startTime;
        
        if (lastDeleteTime > 0)
            startTime = dispatch_time(lastDeleteTime, interval);
        else
            startTime = dispatch_time(DISPATCH_TIME_NOW, interval);
        
        dispatch_source_set_timer(deleteTimer, startTime, interval, 1.0);
    }
}

- (void)createAndStartDeleteTimer
{
    if ((deleteTimer == NULL) && (deleteInterval > 0.0) && (maxAge > 0.0))
    {
        deleteTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, loggerQueue);

        if (deleteTimer != NULL) {
            dispatch_source_set_event_handler(deleteTimer, ^{ @autoreleasepool {

                [self performDelete];

            }});

            [self updateDeleteTimer];
            
            if (deleteTimer != NULL) dispatch_resume(deleteTimer);
        }
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Configuration
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (NSUInteger)saveThreshold
{
    // The design of this method is taken from the DDAbstractLogger implementation.
    // For extensive documentation please refer to the DDAbstractLogger implementation.
    
    // Note: The internal implementation MUST access the colorsEnabled variable directly,
    // This method is designed explicitly for external access.
    //
    // Using "self." syntax to go through this method will cause immediate deadlock.
    // This is the intended result. Fix it by accessing the ivar directly.
    // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
    
    NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
    NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
    
    dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
    
    __block NSUInteger result;
    
    dispatch_sync(globalLoggingQueue, ^{
        dispatch_sync(loggerQueue, ^{
            result = saveThreshold;
        });
    });
    
    return result;
}

- (void)setSaveThreshold:(NSUInteger)threshold
{
    dispatch_block_t block = ^{ @autoreleasepool {
        
        if (saveThreshold != threshold)
        {
            saveThreshold = threshold;
            
            // Since the saveThreshold has changed,
            // we check to see if the current unsavedCount has surpassed the new threshold.
            // 
            // If it has, we immediately save the log.
            
            if ((unsavedCount >= saveThreshold) && (saveThreshold > 0))
            {
                [self performSaveAndSuspendSaveTimer];
            }
        }
    }};
    
    // The design of the setter logic below is taken from the DDAbstractLogger implementation.
    // For documentation please refer to the DDAbstractLogger implementation.
    
    if ([self isOnInternalLoggerQueue])
    {
        block();
    }
    else
    {
        dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
        NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
        
        dispatch_async(globalLoggingQueue, ^{
            dispatch_async(loggerQueue, block);
        });
    }
}

- (NSTimeInterval)saveInterval
{
    // The design of this method is taken from the DDAbstractLogger implementation.
    // For extensive documentation please refer to the DDAbstractLogger implementation.
    
    // Note: The internal implementation MUST access the colorsEnabled variable directly,
    // This method is designed explicitly for external access.
    //
    // Using "self." syntax to go through this method will cause immediate deadlock.
    // This is the intended result. Fix it by accessing the ivar directly.
    // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
    
    NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
    NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
    
    dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
    
    __block NSTimeInterval result;
    
    dispatch_sync(globalLoggingQueue, ^{
        dispatch_sync(loggerQueue, ^{
            result = saveInterval;
        });
    });
    
    return result;
}

- (void)setSaveInterval:(NSTimeInterval)interval
{
    dispatch_block_t block = ^{ @autoreleasepool {
    
        // C99 recommended floating point comparison macro
        // Read: isLessThanOrGreaterThan(floatA, floatB)
        
        if (/* saveInterval != interval */ islessgreater(saveInterval, interval))
        {
            saveInterval = interval;
            
            // There are several cases we need to handle here.
            // 
            // 1. If the saveInterval was previously enabled and it just got disabled,
            //    then we need to stop the saveTimer. (And we might as well release it.)
            // 
            // 2. If the saveInterval was previously disabled and it just got enabled,
            //    then we need to setup the saveTimer. (Plus we might need to do an immediate save.)
            // 
            // 3. If the saveInterval increased, then we need to reset the timer so that it fires at the later date.
            // 
            // 4. If the saveInterval decreased, then we need to reset the timer so that it fires at an earlier date.
            //    (Plus we might need to do an immediate save.)
            
            if (saveInterval > 0.0)
            {
                if (saveTimer == NULL)
                {
                    // Handles #2
                    //
                    // Since the saveTimer uses the unsavedTime to calculate it's first fireDate,
                    // if a save is needed the timer will fire immediately.
                    
                    [self createSuspendedSaveTimer];
                    [self updateAndResumeSaveTimer];
                }
                else
                {
                    // Handles #3
                    // Handles #4
                    //
                    // Since the saveTimer uses the unsavedTime to calculate it's first fireDate,
                    // if a save is needed the timer will fire immediately.
                    
                    [self updateAndResumeSaveTimer];
                }
            }
            else if (saveTimer)
            {
                // Handles #1
                
                [self destroySaveTimer];
            }
        }
    }};
    
    // The design of the setter logic below is taken from the DDAbstractLogger implementation.
    // For documentation please refer to the DDAbstractLogger implementation.
    
    if ([self isOnInternalLoggerQueue])
    {
        block();
    }
    else
    {
        dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
        NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
        
        dispatch_async(globalLoggingQueue, ^{
            dispatch_async(loggerQueue, block);
        });
    }
}

- (NSTimeInterval)maxAge
{
    // The design of this method is taken from the DDAbstractLogger implementation.
    // For extensive documentation please refer to the DDAbstractLogger implementation.
    
    // Note: The internal implementation MUST access the colorsEnabled variable directly,
    // This method is designed explicitly for external access.
    //
    // Using "self." syntax to go through this method will cause immediate deadlock.
    // This is the intended result. Fix it by accessing the ivar directly.
    // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
    
    NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
    NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
    
    dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
    
    __block NSTimeInterval result;
    
    dispatch_sync(globalLoggingQueue, ^{
        dispatch_sync(loggerQueue, ^{
            result = maxAge;
        });
    });
    
    return result;
}

- (void)setMaxAge:(NSTimeInterval)interval
{
    dispatch_block_t block = ^{ @autoreleasepool {
        
        // C99 recommended floating point comparison macro
        // Read: isLessThanOrGreaterThan(floatA, floatB)
        
        if (/* maxAge != interval */ islessgreater(maxAge, interval))
        {
            NSTimeInterval oldMaxAge = maxAge;
            NSTimeInterval newMaxAge = interval;
            
            maxAge = interval;
            
            // There are several cases we need to handle here.
            // 
            // 1. If the maxAge was previously enabled and it just got disabled,
            //    then we need to stop the deleteTimer. (And we might as well release it.)
            // 
            // 2. If the maxAge was previously disabled and it just got enabled,
            //    then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.)
            // 
            // 3. If the maxAge was increased,
            //    then we don't need to do anything.
            // 
            // 4. If the maxAge was decreased,
            //    then we should do an immediate delete.
            
            BOOL shouldDeleteNow = NO;
            
            if (oldMaxAge > 0.0)
            {
                if (newMaxAge <= 0.0)
                {
                    // Handles #1
                    
                    [self destroyDeleteTimer];
                }
                else if (oldMaxAge > newMaxAge)
                {
                    // Handles #4
                    shouldDeleteNow = YES;
                }
            }
            else if (newMaxAge > 0.0)
            {
                // Handles #2
                shouldDeleteNow = YES;
            }
            
            if (shouldDeleteNow)
            {
                [self performDelete];
                
                if (deleteTimer)
                    [self updateDeleteTimer];
                else
                    [self createAndStartDeleteTimer];
            }
        }
    }};
    
    // The design of the setter logic below is taken from the DDAbstractLogger implementation.
    // For documentation please refer to the DDAbstractLogger implementation.
    
    if ([self isOnInternalLoggerQueue])
    {
        block();
    }
    else
    {
        dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
        NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
        
        dispatch_async(globalLoggingQueue, ^{
            dispatch_async(loggerQueue, block);
        });
    }
}

- (NSTimeInterval)deleteInterval
{
    // The design of this method is taken from the DDAbstractLogger implementation.
    // For extensive documentation please refer to the DDAbstractLogger implementation.
    
    // Note: The internal implementation MUST access the colorsEnabled variable directly,
    // This method is designed explicitly for external access.
    //
    // Using "self." syntax to go through this method will cause immediate deadlock.
    // This is the intended result. Fix it by accessing the ivar directly.
    // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
    
    NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
    NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
    
    dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
    
    __block NSTimeInterval result;
    
    dispatch_sync(globalLoggingQueue, ^{
        dispatch_sync(loggerQueue, ^{
            result = deleteInterval;
        });
    });
    
    return result;
}

- (void)setDeleteInterval:(NSTimeInterval)interval
{
    dispatch_block_t block = ^{ @autoreleasepool {
        
        // C99 recommended floating point comparison macro
        // Read: isLessThanOrGreaterThan(floatA, floatB)
        
        if (/* deleteInterval != interval */ islessgreater(deleteInterval, interval))
        {
            deleteInterval = interval;
            
            // There are several cases we need to handle here.
            // 
            // 1. If the deleteInterval was previously enabled and it just got disabled,
            //    then we need to stop the deleteTimer. (And we might as well release it.)
            // 
            // 2. If the deleteInterval was previously disabled and it just got enabled,
            //    then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.)
            // 
            // 3. If the deleteInterval increased, then we need to reset the timer so that it fires at the later date.
            // 
            // 4. If the deleteInterval decreased, then we need to reset the timer so that it fires at an earlier date.
            //    (Plus we might need to do an immediate delete.)
            
            if (deleteInterval > 0.0)
            {
                if (deleteTimer == NULL)
                {
                    // Handles #2
                    //
                    // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate,
                    // if a delete is needed the timer will fire immediately.
                    
                    [self createAndStartDeleteTimer];
                }
                else
                {
                    // Handles #3
                    // Handles #4
                    //
                    // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate,
                    // if a save is needed the timer will fire immediately.
                    
                    [self updateDeleteTimer];
                }
            }
            else if (deleteTimer)
            {
                // Handles #1
                
                [self destroyDeleteTimer];
            }
        }
    }};
    
    // The design of the setter logic below is taken from the DDAbstractLogger implementation.
    // For documentation please refer to the DDAbstractLogger implementation.
    
    if ([self isOnInternalLoggerQueue])
    {
        block();
    }
    else
    {
        dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
        NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
        
        dispatch_async(globalLoggingQueue, ^{
            dispatch_async(loggerQueue, block);
        });
    }
}

- (BOOL)deleteOnEverySave
{
    // The design of this method is taken from the DDAbstractLogger implementation.
    // For extensive documentation please refer to the DDAbstractLogger implementation.
    
    // Note: The internal implementation MUST access the colorsEnabled variable directly,
    // This method is designed explicitly for external access.
    //
    // Using "self." syntax to go through this method will cause immediate deadlock.
    // This is the intended result. Fix it by accessing the ivar directly.
    // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster.
    
    NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
    NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax.");
    
    dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
    
    __block BOOL result;
    
    dispatch_sync(globalLoggingQueue, ^{
        dispatch_sync(loggerQueue, ^{
            result = deleteOnEverySave;
        });
    });
    
    return result;
}

- (void)setDeleteOnEverySave:(BOOL)flag
{
    dispatch_block_t block = ^{
        
        deleteOnEverySave = flag;
    };
    
    // The design of the setter logic below is taken from the DDAbstractLogger implementation.
    // For documentation please refer to the DDAbstractLogger implementation.
    
    if ([self isOnInternalLoggerQueue])
    {
        block();
    }
    else
    {
        dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue];
        NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
        
        dispatch_async(globalLoggingQueue, ^{
            dispatch_async(loggerQueue, block);
        });
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Public API
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (void)savePendingLogEntries
{
    dispatch_block_t block = ^{ @autoreleasepool {
        
        [self performSaveAndSuspendSaveTimer];
    }};
    
    if ([self isOnInternalLoggerQueue])
        block();
    else
        dispatch_async(loggerQueue, block);
}

- (void)deleteOldLogEntries
{
    dispatch_block_t block = ^{ @autoreleasepool {
        
        [self performDelete];
    }};
    
    if ([self isOnInternalLoggerQueue])
        block();
    else
        dispatch_async(loggerQueue, block);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark DDLogger
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (void)didAddLogger
{
    // If you override me be sure to invoke [super didAddLogger];
    
    [self createSuspendedSaveTimer];
    
    [self createAndStartDeleteTimer];
}

- (void)willRemoveLogger
{
    // If you override me be sure to invoke [super willRemoveLogger];
    
    [self performSaveAndSuspendSaveTimer];
    
    [self destroySaveTimer];
    [self destroyDeleteTimer];
}

- (void)logMessage:(DDLogMessage *)logMessage
{
    if ([self db_log:logMessage])
    {
        BOOL firstUnsavedEntry = (++unsavedCount == 1);
        
        if ((unsavedCount >= saveThreshold) && (saveThreshold > 0))
        {
            [self performSaveAndSuspendSaveTimer];
        }
        else if (firstUnsavedEntry)
        {
            unsavedTime = dispatch_time(DISPATCH_TIME_NOW, 0);
            [self updateAndResumeSaveTimer];
        }
    }
}

- (void)flush
{
    // This method is invoked by DDLog's flushLog method.
    // 
    // It is called automatically when the application quits,
    // or if the developer invokes DDLog's flushLog method prior to crashing or something.
    
    [self performSaveAndSuspendSaveTimer];
}

@end
