#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/robbiehanson/CocoaLumberjack
 * 
 * If you're new to the project you may wish to read the "Getting Started" wiki.
 * https://github.com/robbiehanson/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];
            
            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
