//
//  LIFEFileLogger.m
//  Copyright (C) 2017 Buglife, Inc.
//  
//  Licensed 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.
//
//

// Software License Agreement (BSD License)
//
// Copyright (c) 2010-2016, Deusty, LLC
// All rights reserved.
//
// Redistribution and use of this software in source and binary forms,
// with or without modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
//   this list of conditions and the following disclaimer.
//
// * Neither the name of Deusty nor the names of its contributors may be used
//   to endorse or promote products derived from this software without specific
//   prior written permission of Deusty, LLC.


#import "LIFEFileLogger.h"

#import <unistd.h>
#import <sys/attr.h>
#import <sys/xattr.h>
#import <libkern/OSAtomic.h>

// We probably shouldn't be using LIFELogImpl() statements within the LIFELogImpl implementation.
// But we still want to leave our log statements for any future debugging,
// and to allow other developers to trace the implementation (which is a great learning tool).
//
// So we use primitive logging macros around NSLog.
// We maintain the NS prefix on the macros to be explicit about the fact that we're using NSLog.

#ifndef DD_NSLOG_LEVEL
#define DD_NSLOG_LEVEL 2
#endif

#define NSLogError(frmt, ...)    do{ if(DD_NSLOG_LEVEL >= 1) NSLog((frmt), ##__VA_ARGS__); } while(0)
#define NSLogWarn(frmt, ...)     do{ if(DD_NSLOG_LEVEL >= 2) NSLog((frmt), ##__VA_ARGS__); } while(0)
#define NSLogInfo(frmt, ...)     do{ if(DD_NSLOG_LEVEL >= 3) NSLog((frmt), ##__VA_ARGS__); } while(0)
#define NSLogDebug(frmt, ...)    do{ if(DD_NSLOG_LEVEL >= 4) NSLog((frmt), ##__VA_ARGS__); } while(0)
#define NSLogVerbose(frmt, ...)  do{ if(DD_NSLOG_LEVEL >= 5) NSLog((frmt), ##__VA_ARGS__); } while(0)

#if TARGET_OS_IPHONE
BOOL life_doesAppRunInBackground(void);
#endif

unsigned long long const kLIFEDefaultLogMaxFileSize      = 1024 * 1024;      // 1 MB
NSTimeInterval     const kLIFEDefaultLogRollingFrequency = 60 * 60 * 24;     // 24 Hours
NSUInteger         const kLIFEDefaultLogMaxNumLogFiles   = 5;                // 5 Files
unsigned long long const kLIFEDefaultLogFilesDiskQuota   = 20 * 1024 * 1024; // 20 MB

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

@interface LIFELogFileManagerDefault () {
    NSUInteger _maximumNumberOfLogFiles;
    unsigned long long _logFilesDiskQuota;
    NSString *_logsDirectory;
#if TARGET_OS_IPHONE
    NSString *_defaultFileProtectionLevel;
#endif
}

- (void)deleteOldLogFiles;
- (NSString *)defaultLogsDirectory;

@end

@implementation LIFELogFileManagerDefault

@synthesize maximumNumberOfLogFiles = _maximumNumberOfLogFiles;
@synthesize logFilesDiskQuota = _logFilesDiskQuota;


- (instancetype)init {
    return [self initWithLogsDirectory:nil];
}

- (instancetype)initWithLogsDirectory:(NSString *)aLogsDirectory {
    if ((self = [super init])) {
        _maximumNumberOfLogFiles = kLIFEDefaultLogMaxNumLogFiles;
        _logFilesDiskQuota = kLIFEDefaultLogFilesDiskQuota;
        
        if (aLogsDirectory) {
            _logsDirectory = [aLogsDirectory copy];
        } else {
            _logsDirectory = [[self defaultLogsDirectory] copy];
        }
        
        NSKeyValueObservingOptions kvoOptions = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
        
        [self addObserver:self forKeyPath:NSStringFromSelector(@selector(maximumNumberOfLogFiles)) options:kvoOptions context:nil];
        [self addObserver:self forKeyPath:NSStringFromSelector(@selector(logFilesDiskQuota)) options:kvoOptions context:nil];
        
        NSLogVerbose(@"DDFileLogManagerDefault: logsDirectory:\n%@", [self logsDirectory]);
        NSLogVerbose(@"DDFileLogManagerDefault: sortedLogFileNames:\n%@", [self sortedLogFileNames]);
    }
    
    return self;
}

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey
{
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"maximumNumberOfLogFiles"] || [theKey isEqualToString:@"logFilesDiskQuota"]) {
        automatic = NO;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    
    return automatic;
}

#if TARGET_OS_IPHONE
- (instancetype)initWithLogsDirectory:(NSString *)logsDirectory defaultFileProtectionLevel:(NSString *)fileProtectionLevel {
    if ((self = [self initWithLogsDirectory:logsDirectory])) {
        if ([fileProtectionLevel isEqualToString:NSFileProtectionNone] ||
            [fileProtectionLevel isEqualToString:NSFileProtectionComplete] ||
            [fileProtectionLevel isEqualToString:NSFileProtectionCompleteUnlessOpen] ||
            [fileProtectionLevel isEqualToString:NSFileProtectionCompleteUntilFirstUserAuthentication]) {
            _defaultFileProtectionLevel = fileProtectionLevel;
        }
    }
    
    return self;
}

#endif

- (void)dealloc {
    // try-catch because the observer might be removed or never added. In this case, removeObserver throws and exception
    @try {
        [self removeObserver:self forKeyPath:NSStringFromSelector(@selector(maximumNumberOfLogFiles))];
        [self removeObserver:self forKeyPath:NSStringFromSelector(@selector(logFilesDiskQuota))];
    } @catch (NSException *exception) {
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Configuration
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    NSNumber *old = change[NSKeyValueChangeOldKey];
    NSNumber *new = change[NSKeyValueChangeNewKey];
    
    if ([old isEqual:new]) {
        // No change in value - don't bother with any processing.
        return;
    }
    
    if ([keyPath isEqualToString:NSStringFromSelector(@selector(maximumNumberOfLogFiles))] ||
        [keyPath isEqualToString:NSStringFromSelector(@selector(logFilesDiskQuota))]) {
        NSLogInfo(@"DDFileLogManagerDefault: Responding to configuration change: %@", keyPath);
        
        dispatch_async([LIFELogImpl loggingQueue], ^{ @autoreleasepool {
            [self deleteOldLogFiles];
        } });
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark File Deleting
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Deletes archived log files that exceed the maximumNumberOfLogFiles or logFilesDiskQuota configuration values.
 **/
- (void)deleteOldLogFiles {
    NSLogVerbose(@"LIFELogFileManagerDefault: deleteOldLogFiles");
    
    NSArray *sortedLogFileInfos = [self sortedLogFileInfos];
    
    NSUInteger firstIndexToDelete = NSNotFound;
    
    const unsigned long long diskQuota = self.logFilesDiskQuota;
    const NSUInteger maxNumLogFiles = self.maximumNumberOfLogFiles;
    
    if (diskQuota) {
        unsigned long long used = 0;
        
        for (NSUInteger i = 0; i < sortedLogFileInfos.count; i++) {
            LIFELogFileInfo *info = sortedLogFileInfos[i];
            used += info.fileSize;
            
            if (used > diskQuota) {
                firstIndexToDelete = i;
                break;
            }
        }
    }
    
    if (maxNumLogFiles) {
        if (firstIndexToDelete == NSNotFound) {
            firstIndexToDelete = maxNumLogFiles;
        } else {
            firstIndexToDelete = MIN(firstIndexToDelete, maxNumLogFiles);
        }
    }
    
    if (firstIndexToDelete == 0) {
        // Do we consider the first file?
        // We are only supposed to be deleting archived files.
        // In most cases, the first file is likely the log file that is currently being written to.
        // So in most cases, we do not want to consider this file for deletion.
        
        if (sortedLogFileInfos.count > 0) {
            LIFELogFileInfo *logFileInfo = sortedLogFileInfos[0];
            
            if (!logFileInfo.isArchived) {
                // Don't delete active file.
                ++firstIndexToDelete;
            }
        }
    }
    
    if (firstIndexToDelete != NSNotFound) {
        // removing all logfiles starting with firstIndexToDelete
        
        for (NSUInteger i = firstIndexToDelete; i < sortedLogFileInfos.count; i++) {
            LIFELogFileInfo *logFileInfo = sortedLogFileInfos[i];
            
            NSLogInfo(@"LIFELogFileManagerDefault: Deleting file: %@", logFileInfo.fileName);
            
            [[NSFileManager defaultManager] removeItemAtPath:logFileInfo.filePath error:nil];
        }
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Log Files
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Returns the path to the default logs directory.
 * If the logs directory doesn't exist, this method automatically creates it.
 **/
- (NSString *)defaultLogsDirectory {
#if TARGET_OS_IPHONE
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    NSString *baseDir = paths.firstObject;
    NSString *logsDirectory = [baseDir stringByAppendingPathComponent:@"BuglifeLogs"];
    
#else
    NSString *appName = [[NSProcessInfo processInfo] processName];
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
    NSString *basePath = ([paths count] > 0) ? paths[0] : NSTemporaryDirectory();
    NSString *logsDirectory = [[basePath stringByAppendingPathComponent:@"BuglifeLogs"] stringByAppendingPathComponent:appName];
    
#endif
    
    return logsDirectory;
}

- (NSString *)logsDirectory {
    // We could do this check once, during initalization, and not bother again.
    // But this way the code continues to work if the directory gets deleted while the code is running.
    
    if (![[NSFileManager defaultManager] fileExistsAtPath:_logsDirectory]) {
        NSError *err = nil;
        
        if (![[NSFileManager defaultManager] createDirectoryAtPath:_logsDirectory
                                       withIntermediateDirectories:YES
                                                        attributes:nil
                                                             error:&err]) {
            NSLogError(@"DDFileLogManagerDefault: Error creating logsDirectory: %@", err);
        }
    }
    
    return _logsDirectory;
}

- (BOOL)isLogFile:(NSString *)fileName {
    NSString *appName = [self applicationName];
    
    BOOL hasProperPrefix = [fileName hasPrefix:appName];
    BOOL hasProperSuffix = [fileName hasSuffix:@".log"];
    BOOL hasProperDate = NO;
    
    if (hasProperPrefix && hasProperSuffix) {
        NSUInteger lengthOfMiddle = fileName.length - appName.length - @".log".length;
        
        // Date string should have at least 16 characters - " 2013-12-03 17-14"
        if (lengthOfMiddle >= 17) {
            NSRange range = NSMakeRange(appName.length, lengthOfMiddle);
            
            NSString *middle = [fileName substringWithRange:range];
            NSArray *components = [middle componentsSeparatedByString:@" "];
            
            // When creating logfile if there is existing file with the same name, we append attemp number at the end.
            // Thats why here we can have three or four components. For details see createNewLogFile method.
            //
            // Components:
            //     "", "2013-12-03", "17-14"
            // or
            //     "", "2013-12-03", "17-14", "1"
            if (components.count == 3 || components.count == 4) {
                NSString *dateString = [NSString stringWithFormat:@"%@ %@", components[1], components[2]];
                NSDateFormatter *dateFormatter = [self logFileDateFormatter];
                
                NSDate *date = [dateFormatter dateFromString:dateString];
                
                if (date) {
                    hasProperDate = YES;
                }
            }
        }
    }
    
    return (hasProperPrefix && hasProperDate && hasProperSuffix);
}

- (NSDateFormatter *)logFileDateFormatter {
    NSMutableDictionary *dictionary = [[NSThread currentThread]
                                       threadDictionary];
    NSString *dateFormat = @"yyyy'-'MM'-'dd' 'HH'-'mm'";
    NSString *key = [NSString stringWithFormat:@"logFileDateFormatter.%@", dateFormat];
    NSDateFormatter *dateFormatter = dictionary[key];
    
    if (dateFormatter == nil) {
        dateFormatter = [[NSDateFormatter alloc] init];
        [dateFormatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]];
        [dateFormatter setDateFormat:dateFormat];
        [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
        dictionary[key] = dateFormatter;
    }
    
    return dateFormatter;
}

- (NSArray *)unsortedLogFilePaths {
    NSString *logsDirectory = [self logsDirectory];
    NSArray *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:logsDirectory error:nil];
    
    NSMutableArray *unsortedLogFilePaths = [NSMutableArray arrayWithCapacity:[fileNames count]];
    
    for (NSString *fileName in fileNames) {
        // Filter out any files that aren't log files. (Just for extra safety)
        
#if TARGET_IPHONE_SIMULATOR
        // In case of iPhone simulator there can be 'archived' extension. isLogFile:
        // method knows nothing about it. Thus removing it for this method.
        //
        // See full explanation in the header file.
        NSString *theFileName = [fileName stringByReplacingOccurrencesOfString:@".archived"
                                                                    withString:@""];
        
        if ([self isLogFile:theFileName])
#else
            
            if ([self isLogFile:fileName])
#endif
            {
                NSString *filePath = [logsDirectory stringByAppendingPathComponent:fileName];
                
                [unsortedLogFilePaths addObject:filePath];
            }
    }
    
    return unsortedLogFilePaths;
}

- (NSArray *)unsortedLogFileNames {
    NSArray *unsortedLogFilePaths = [self unsortedLogFilePaths];
    
    NSMutableArray *unsortedLogFileNames = [NSMutableArray arrayWithCapacity:[unsortedLogFilePaths count]];
    
    for (NSString *filePath in unsortedLogFilePaths) {
        [unsortedLogFileNames addObject:[filePath lastPathComponent]];
    }
    
    return unsortedLogFileNames;
}

- (NSArray *)unsortedLogFileInfos {
    NSArray *unsortedLogFilePaths = [self unsortedLogFilePaths];
    
    NSMutableArray *unsortedLogFileInfos = [NSMutableArray arrayWithCapacity:[unsortedLogFilePaths count]];
    
    for (NSString *filePath in unsortedLogFilePaths) {
        LIFELogFileInfo *logFileInfo = [[LIFELogFileInfo alloc] initWithFilePath:filePath];
        
        [unsortedLogFileInfos addObject:logFileInfo];
    }
    
    return unsortedLogFileInfos;
}

- (NSArray *)sortedLogFilePaths {
    NSArray *sortedLogFileInfos = [self sortedLogFileInfos];
    
    NSMutableArray *sortedLogFilePaths = [NSMutableArray arrayWithCapacity:[sortedLogFileInfos count]];
    
    for (LIFELogFileInfo *logFileInfo in sortedLogFileInfos) {
        [sortedLogFilePaths addObject:[logFileInfo filePath]];
    }
    
    return sortedLogFilePaths;
}

- (NSArray *)sortedLogFileNames {
    NSArray *sortedLogFileInfos = [self sortedLogFileInfos];
    
    NSMutableArray *sortedLogFileNames = [NSMutableArray arrayWithCapacity:[sortedLogFileInfos count]];
    
    for (LIFELogFileInfo *logFileInfo in sortedLogFileInfos) {
        [sortedLogFileNames addObject:[logFileInfo fileName]];
    }
    
    return sortedLogFileNames;
}

- (NSArray *)sortedLogFileInfos {
    return [[self unsortedLogFileInfos] sortedArrayUsingSelector:@selector(reverseCompareByCreationDate:)];
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Creation
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (NSString *)newLogFileName {
    NSString *appName = [self applicationName];
    
    NSDateFormatter *dateFormatter = [self logFileDateFormatter];
    NSString *formattedDate = [dateFormatter stringFromDate:[NSDate date]];
    
    return [NSString stringWithFormat:@"%@ %@.log", appName, formattedDate];
}

- (NSString *)createNewLogFile {
    NSString *fileName = [self newLogFileName];
    NSString *logsDirectory = [self logsDirectory];
    
    NSUInteger attempt = 1;
    
    do {
        NSString *actualFileName = fileName;
        
        if (attempt > 1) {
            NSString *extension = [actualFileName pathExtension];
            
            actualFileName = [actualFileName stringByDeletingPathExtension];
            actualFileName = [actualFileName stringByAppendingFormat:@" %lu", (unsigned long)attempt];
            
            if (extension.length) {
                actualFileName = [actualFileName stringByAppendingPathExtension:extension];
            }
        }
        
        NSString *filePath = [logsDirectory stringByAppendingPathComponent:actualFileName];
        
        if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
            NSLogVerbose(@"LIFELogFileManagerDefault: Creating new log file: %@", actualFileName);
            
            NSDictionary *attributes = nil;
            
#if TARGET_OS_IPHONE
            // When creating log file on iOS we're setting NSFileProtectionKey attribute to NSFileProtectionCompleteUnlessOpen.
            //
            // But in case if app is able to launch from background we need to have an ability to open log file any time we
            // want (even if device is locked). Thats why that attribute have to be changed to
            // NSFileProtectionCompleteUntilFirstUserAuthentication.
            
            NSString *key = _defaultFileProtectionLevel ? :
            (life_doesAppRunInBackground() ? NSFileProtectionCompleteUntilFirstUserAuthentication : NSFileProtectionCompleteUnlessOpen);
            
            attributes = @{
                           NSFileProtectionKey: key
                           };
#endif
            
            [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:attributes];
            
            // Since we just created a new log file, we may need to delete some old log files
            [self deleteOldLogFiles];
            
            return filePath;
        } else {
            attempt++;
        }
    } while (YES);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Utility
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (NSString *)applicationName {
    static NSString *_appName;
    static dispatch_once_t onceToken;
    
    dispatch_once(&onceToken, ^{
        _appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"];
        
        if (!_appName) {
            _appName = [[NSProcessInfo processInfo] processName];
        }
        
        if (!_appName) {
            _appName = @"";
        }
    });
    
    return _appName;
}

@end




////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

@interface LIFEFileLogger () {
    __strong id <LIFELogFileManager> _logFileManager;
    
    LIFELogFileInfo *_currentLogFileInfo;
    NSFileHandle *_currentLogFileHandle;
    
    dispatch_source_t _currentLogFileVnode;
    dispatch_source_t _rollingTimer;
    
    unsigned long long _maximumFileSize;
    NSTimeInterval _rollingFrequency;
}

- (void)rollLogFileNow;
- (void)maybeRollLogFileDueToAge;
- (void)maybeRollLogFileDueToSize;

@end

@implementation LIFEFileLogger

- (instancetype)initWithLogFormatter:(id <LIFELogFormatter>)aLogFormatter {
    LIFELogFileManagerDefault *defaultLogFileManager = [[LIFELogFileManagerDefault alloc] init];
    
    return [self initWithLogFileManager:defaultLogFileManager logFormatter:aLogFormatter];
}

- (instancetype)initWithLogFileManager:(id <LIFELogFileManager>)aLogFileManager logFormatter:(id <LIFELogFormatter>)aLogFormatter {
    if ((self = [super init])) {
        _maximumFileSize = kLIFEDefaultLogMaxFileSize;
        _rollingFrequency = kLIFEDefaultLogRollingFrequency;
        _automaticallyAppendNewlineForCustomFormatters = YES;
        
        logFileManager = aLogFileManager;

        self.logFormatter = aLogFormatter;
    }
    
    return self;
}

- (void)dealloc {
    [_currentLogFileHandle synchronizeFile];
    [_currentLogFileHandle closeFile];
    
    if (_currentLogFileVnode) {
        dispatch_source_cancel(_currentLogFileVnode);
        _currentLogFileVnode = NULL;
    }
    
    if (_rollingTimer) {
        dispatch_source_cancel(_rollingTimer);
        _rollingTimer = NULL;
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Properties
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

@synthesize logFileManager;

- (unsigned long long)maximumFileSize {
    __block unsigned long long result;
    
    dispatch_block_t block = ^{
        result = _maximumFileSize;
    };
    
    // The design of this method is taken from the LIFEAbstractLogger implementation.
    // For extensive documentation please refer to the LIFEAbstractLogger implementation.
    
    // Note: The internal implementation MUST access the maximumFileSize 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 = [LIFELogImpl loggingQueue];
    
    dispatch_sync(globalLoggingQueue, ^{
        dispatch_sync(self.loggerQueue, block);
    });
    
    return result;
}

- (void)setMaximumFileSize:(unsigned long long)newMaximumFileSize {
    dispatch_block_t block = ^{
        @autoreleasepool {
            _maximumFileSize = newMaximumFileSize;
            [self maybeRollLogFileDueToSize];
        }
    };
    
    // The design of this method is taken from the LIFEAbstractLogger implementation.
    // For extensive documentation please refer to the LIFEAbstractLogger implementation.
    
    // Note: The internal implementation MUST access the maximumFileSize 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 = [LIFELogImpl loggingQueue];
    
    dispatch_async(globalLoggingQueue, ^{
        dispatch_async(self.loggerQueue, block);
    });
}

- (NSTimeInterval)rollingFrequency {
    __block NSTimeInterval result;
    
    dispatch_block_t block = ^{
        result = _rollingFrequency;
    };
    
    // The design of this method is taken from the LIFEAbstractLogger implementation.
    // For extensive documentation please refer to the LIFEAbstractLogger implementation.
    
    // Note: The internal implementation should access the rollingFrequency 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 = [LIFELogImpl loggingQueue];
    
    dispatch_sync(globalLoggingQueue, ^{
        dispatch_sync(self.loggerQueue, block);
    });
    
    return result;
}

- (void)setRollingFrequency:(NSTimeInterval)newRollingFrequency {
    dispatch_block_t block = ^{
        @autoreleasepool {
            _rollingFrequency = newRollingFrequency;
            [self maybeRollLogFileDueToAge];
        }
    };
    
    // The design of this method is taken from the LIFEAbstractLogger implementation.
    // For extensive documentation please refer to the LIFEAbstractLogger implementation.
    
    // Note: The internal implementation should access the rollingFrequency 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 = [LIFELogImpl loggingQueue];
    
    dispatch_async(globalLoggingQueue, ^{
        dispatch_async(self.loggerQueue, block);
    });
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark File Rolling
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (void)scheduleTimerToRollLogFileDueToAge {
    if (_rollingTimer) {
        dispatch_source_cancel(_rollingTimer);
        _rollingTimer = NULL;
    }
    
    if (_currentLogFileInfo == nil || _rollingFrequency <= 0.0) {
        return;
    }
    
    NSDate *logFileCreationDate = [_currentLogFileInfo creationDate];
    
    NSTimeInterval ti = [logFileCreationDate timeIntervalSinceReferenceDate];
    ti += _rollingFrequency;
    
    NSDate *logFileRollingDate = [NSDate dateWithTimeIntervalSinceReferenceDate:ti];
    
    NSLogVerbose(@"LIFEFileLogger: scheduleTimerToRollLogFileDueToAge");
    
    NSLogVerbose(@"LIFEFileLogger: logFileCreationDate: %@", logFileCreationDate);
    NSLogVerbose(@"LIFEFileLogger: logFileRollingDate : %@", logFileRollingDate);
    
    _rollingTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.loggerQueue);
    
    dispatch_source_set_event_handler(_rollingTimer, ^{ @autoreleasepool {
        [self maybeRollLogFileDueToAge];
    } });
    
#if !OS_OBJECT_USE_OBJC
    dispatch_source_t theRollingTimer = _rollingTimer;
    dispatch_source_set_cancel_handler(_rollingTimer, ^{
        dispatch_release(theRollingTimer);
    });
#endif
    
    uint64_t delay = (uint64_t)([logFileRollingDate timeIntervalSinceNow] * NSEC_PER_SEC);
    dispatch_time_t fireTime = dispatch_time(DISPATCH_TIME_NOW, delay);
    
    dispatch_source_set_timer(_rollingTimer, fireTime, DISPATCH_TIME_FOREVER, 1.0);
    dispatch_resume(_rollingTimer);
}

- (void)rollLogFile {
    [self rollLogFileWithCompletionBlock:nil];
}

- (void)rollLogFileWithCompletionBlock:(void (^)(void))completionBlock {
    [self rollLogFileToQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) completionBlock:completionBlock];
}

- (void)rollLogFileToQueue:(dispatch_queue_t)completionQueue completionBlock:(void (^)(void))completionBlock
{
    // This method is public.
    // We need to execute the rolling on our logging thread/queue.
    
    dispatch_block_t block = ^{
        @autoreleasepool {
            [self rollLogFileNow];
            
            if (completionBlock) {
                dispatch_async(completionQueue, ^{
                    completionBlock();
                });
            }
        }
    };
    
    // The design of this method is taken from the LIFEAbstractLogger implementation.
    // For extensive documentation please refer to the LIFEAbstractLogger implementation.
    
    if ([self isOnInternalLoggerQueue]) {
        block();
    } else {
        dispatch_queue_t globalLoggingQueue = [LIFELogImpl loggingQueue];
        NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure");
        
        dispatch_async(globalLoggingQueue, ^{
            dispatch_async(self.loggerQueue, block);
        });
    }
}

- (void)rollLogFileNow {
    NSLogVerbose(@"LIFEFileLogger: rollLogFileNow");
    
    if (_currentLogFileHandle == nil) {
        return;
    }
    
    [_currentLogFileHandle synchronizeFile];
    [_currentLogFileHandle closeFile];
    _currentLogFileHandle = nil;
    
    _currentLogFileInfo.isArchived = YES;
    
    if ([logFileManager respondsToSelector:@selector(didRollAndArchiveLogFile:)]) {
        [logFileManager didRollAndArchiveLogFile:(_currentLogFileInfo.filePath)];
    }
    
    _currentLogFileInfo = nil;
    
    if (_currentLogFileVnode) {
        dispatch_source_cancel(_currentLogFileVnode);
        _currentLogFileVnode = NULL;
    }
    
    if (_rollingTimer) {
        dispatch_source_cancel(_rollingTimer);
        _rollingTimer = NULL;
    }
}

- (void)maybeRollLogFileDueToAge {
    if (_rollingFrequency > 0.0 && _currentLogFileInfo.age >= _rollingFrequency) {
        NSLogVerbose(@"LIFEFileLogger: Rolling log file due to age...");
        
        [self rollLogFileNow];
    } else {
        [self scheduleTimerToRollLogFileDueToAge];
    }
}

- (void)maybeRollLogFileDueToSize {
    // This method is called from logMessage.
    // Keep it FAST.
    
    // Note: Use direct access to maximumFileSize variable.
    // We specifically wrote our own getter/setter method to allow us to do this (for performance reasons).
    
    if (_maximumFileSize > 0) {
        unsigned long long fileSize = [_currentLogFileHandle offsetInFile];
        
        if (fileSize >= _maximumFileSize) {
            NSLogVerbose(@"LIFEFileLogger: Rolling log file due to size (%qu)...", fileSize);
            
            [self rollLogFileNow];
        }
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark File Logging
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Returns the log file that should be used.
 * If there is an existing log file that is suitable,
 * within the constraints of maximumFileSize and rollingFrequency, then it is returned.
 *
 * Otherwise a new file is created and returned.
 **/
- (LIFELogFileInfo *)currentLogFileInfo {
    if (_currentLogFileInfo == nil) {
        NSArray *sortedLogFileInfos = [logFileManager sortedLogFileInfos];
        
        if ([sortedLogFileInfos count] > 0) {
            LIFELogFileInfo *mostRecentLogFileInfo = sortedLogFileInfos[0];
            
            BOOL shouldArchiveMostRecent = NO;
            
            if (mostRecentLogFileInfo.isArchived) {
                shouldArchiveMostRecent = NO;
            } else if (_maximumFileSize > 0 && mostRecentLogFileInfo.fileSize >= _maximumFileSize) {
                shouldArchiveMostRecent = YES;
            } else if (_rollingFrequency > 0.0 && mostRecentLogFileInfo.age >= _rollingFrequency) {
                shouldArchiveMostRecent = YES;
            }
            
#if TARGET_OS_IPHONE
            // When creating log file on iOS we're setting NSFileProtectionKey attribute to NSFileProtectionCompleteUnlessOpen.
            //
            // But in case if app is able to launch from background we need to have an ability to open log file any time we
            // want (even if device is locked). Thats why that attribute have to be changed to
            // NSFileProtectionCompleteUntilFirstUserAuthentication.
            //
            // If previous log was created when app wasn't running in background, but now it is - we archive it and create
            // a new one.
            //
            // If user has owerwritten to NSFileProtectionNone there is no neeed to create a new one.
            
            if (!_doNotReuseLogFiles && life_doesAppRunInBackground()) {
                NSString *key = mostRecentLogFileInfo.fileAttributes[NSFileProtectionKey];
                
                if ([key length] > 0 && !([key isEqualToString:NSFileProtectionCompleteUntilFirstUserAuthentication] || [key isEqualToString:NSFileProtectionNone])) {
                    shouldArchiveMostRecent = YES;
                }
            }
            
#endif
            
            if (!_doNotReuseLogFiles && !mostRecentLogFileInfo.isArchived && !shouldArchiveMostRecent) {
                NSLogVerbose(@"LIFEFileLogger: Resuming logging with file %@", mostRecentLogFileInfo.fileName);
                
                _currentLogFileInfo = mostRecentLogFileInfo;
            } else {
                if (shouldArchiveMostRecent) {
                    mostRecentLogFileInfo.isArchived = YES;
                    
                    if ([logFileManager respondsToSelector:@selector(didArchiveLogFile:)]) {
                        [logFileManager didArchiveLogFile:(mostRecentLogFileInfo.filePath)];
                    }
                }
            }
        }
        
        if (_currentLogFileInfo == nil) {
            NSString *currentLogFilePath = [logFileManager createNewLogFile];
            
            _currentLogFileInfo = [[LIFELogFileInfo alloc] initWithFilePath:currentLogFilePath];
        }
    }
    
    return _currentLogFileInfo;
}

- (NSFileHandle *)currentLogFileHandle {
    if (_currentLogFileHandle == nil) {
        NSString *logFilePath = [[self currentLogFileInfo] filePath];
        
        _currentLogFileHandle = [NSFileHandle fileHandleForWritingAtPath:logFilePath];
        [_currentLogFileHandle seekToEndOfFile];
        
        if (_currentLogFileHandle) {
            [self scheduleTimerToRollLogFileDueToAge];
            
            // Here we are monitoring the log file. In case if it would be deleted ormoved
            // somewhere we want to roll it and use a new one.
            _currentLogFileVnode = dispatch_source_create(
                                                          DISPATCH_SOURCE_TYPE_VNODE,
                                                          [_currentLogFileHandle fileDescriptor],
                                                          DISPATCH_VNODE_DELETE | DISPATCH_VNODE_RENAME,
                                                          self.loggerQueue
                                                          );
            
            dispatch_source_set_event_handler(_currentLogFileVnode, ^{ @autoreleasepool {
                NSLogInfo(@"LIFEFileLogger: Current logfile was moved. Rolling it and creating a new one");
                [self rollLogFileNow];
            } });
            
#if !OS_OBJECT_USE_OBJC
            dispatch_source_t vnode = _currentLogFileVnode;
            dispatch_source_set_cancel_handler(_currentLogFileVnode, ^{
                dispatch_release(vnode);
            });
#endif
            
            dispatch_resume(_currentLogFileVnode);
        }
    }
    
    return _currentLogFileHandle;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark DDLogger Protocol
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

static int exception_count = 0;
- (void)logMessage:(LIFELogMessage *)logMessage {
    NSString *message = logMessage->_message;
    BOOL isFormatted = NO;
    
    if (_logFormatter) {
        message = [_logFormatter formatLogMessage:logMessage];
        isFormatted = message != logMessage->_message;
    }
    
    if (message) {
        if ((!isFormatted || _automaticallyAppendNewlineForCustomFormatters) &&
            (![message hasSuffix:@"\n"])) {
            message = [message stringByAppendingString:@"\n"];
        }
        
        NSData *logData = [message dataUsingEncoding:NSUTF8StringEncoding];
        
        @try {
            [[self currentLogFileHandle] writeData:logData];
            
            [self maybeRollLogFileDueToSize];
        } @catch (NSException *exception) {
            exception_count++;
            
            if (exception_count <= 10) {
                NSLogError(@"LIFEFileLogger.logMessage: %@", exception);
                
                if (exception_count == 10) {
                    NSLogError(@"LIFEFileLogger.logMessage: Too many exceptions -- will not log any more of them.");
                }
            }
        }
    }
}

- (void)willRemoveLogger {
    // If you override me be sure to invoke [super willRemoveLogger];
    
    [self rollLogFileNow];
}

- (NSString *)loggerName {
    return @"cocoa.lumberjack.fileLogger";
}

@end


////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

#if TARGET_IPHONE_SIMULATOR
static NSString * const kDDXAttrArchivedName = @"archived";
#else
static NSString * const kDDXAttrArchivedName = @"lumberjack.log.archived";
#endif

@interface LIFELogFileInfo () {
    __strong NSString *_filePath;
    __strong NSString *_fileName;
    
    __strong NSDictionary *_fileAttributes;
    
    __strong NSDate *_creationDate;
    __strong NSDate *_modificationDate;
    
    unsigned long long _fileSize;
}

@end


@implementation LIFELogFileInfo

@synthesize filePath;

@dynamic fileName;
@dynamic fileAttributes;
@dynamic creationDate;
@dynamic modificationDate;
@dynamic fileSize;
@dynamic age;

@dynamic isArchived;


#pragma mark Lifecycle

+ (instancetype)logFileWithPath:(NSString *)aFilePath {
    return [[self alloc] initWithFilePath:aFilePath];
}

- (instancetype)initWithFilePath:(NSString *)aFilePath {
    if ((self = [super init])) {
        filePath = [aFilePath copy];
    }
    
    return self;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Standard Info
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (NSDictionary *)fileAttributes {
    if (_fileAttributes == nil) {
        _fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
    }
    
    return _fileAttributes;
}

- (NSString *)fileName {
    if (_fileName == nil) {
        _fileName = [filePath lastPathComponent];
    }
    
    return _fileName;
}

- (NSDate *)modificationDate {
    if (_modificationDate == nil) {
        _modificationDate = self.fileAttributes[NSFileModificationDate];
    }
    
    return _modificationDate;
}

- (NSDate *)creationDate {
    if (_creationDate == nil) {
        _creationDate = self.fileAttributes[NSFileCreationDate];
    }
    
    return _creationDate;
}

- (unsigned long long)fileSize {
    if (_fileSize == 0) {
        _fileSize = [self.fileAttributes[NSFileSize] unsignedLongLongValue];
    }
    
    return _fileSize;
}

- (NSTimeInterval)age {
    return [[self creationDate] timeIntervalSinceNow] * -1.0;
}

- (NSString *)description {
    return [@{ @"filePath": self.filePath ? : @"",
               @"fileName": self.fileName ? : @"",
               @"fileAttributes": self.fileAttributes ? : @"",
               @"creationDate": self.creationDate ? : @"",
               @"modificationDate": self.modificationDate ? : @"",
               @"fileSize": @(self.fileSize),
               @"age": @(self.age),
               @"isArchived": @(self.isArchived) } description];
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Archiving
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (BOOL)isArchived {
#if TARGET_IPHONE_SIMULATOR
    
    // Extended attributes don't work properly on the simulator.
    // So we have to use a less attractive alternative.
    // See full explanation in the header file.
    
    return [self hasExtensionAttributeWithName:kDDXAttrArchivedName];
    
#else
    
    return [self hasExtendedAttributeWithName:kDDXAttrArchivedName];
    
#endif
}

- (void)setIsArchived:(BOOL)flag {
#if TARGET_IPHONE_SIMULATOR
    
    // Extended attributes don't work properly on the simulator.
    // So we have to use a less attractive alternative.
    // See full explanation in the header file.
    
    if (flag) {
        [self addExtensionAttributeWithName:kDDXAttrArchivedName];
    } else {
        [self removeExtensionAttributeWithName:kDDXAttrArchivedName];
    }
    
#else
    
    if (flag) {
        [self addExtendedAttributeWithName:kDDXAttrArchivedName];
    } else {
        [self removeExtendedAttributeWithName:kDDXAttrArchivedName];
    }
    
#endif
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Changes
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (void)reset {
    _fileName = nil;
    _fileAttributes = nil;
    _creationDate = nil;
    _modificationDate = nil;
}

- (void)renameFile:(NSString *)newFileName {
    // This method is only used on the iPhone simulator, where normal extended attributes are broken.
    // See full explanation in the header file.
    
    if (![newFileName isEqualToString:[self fileName]]) {
        NSString *fileDir = [filePath stringByDeletingLastPathComponent];
        
        NSString *newFilePath = [fileDir stringByAppendingPathComponent:newFileName];
        
        NSLogVerbose(@"LIFELogFileInfo: Renaming file: '%@' -> '%@'", self.fileName, newFileName);
        
        NSError *error = nil;
        
        if ([[NSFileManager defaultManager] fileExistsAtPath:newFilePath] &&
            ![[NSFileManager defaultManager] removeItemAtPath:newFilePath error:&error]) {
            NSLogError(@"LIFELogFileInfo: Error deleting archive (%@): %@", self.fileName, error);
        }
        
        if (![[NSFileManager defaultManager] moveItemAtPath:filePath toPath:newFilePath error:&error]) {
            NSLogError(@"LIFELogFileInfo: Error renaming file (%@): %@", self.fileName, error);
        }
        
        filePath = newFilePath;
        [self reset];
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Attribute Management
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

#if TARGET_IPHONE_SIMULATOR

// Extended attributes don't work properly on the simulator.
// So we have to use a less attractive alternative.
// See full explanation in the header file.

- (BOOL)hasExtensionAttributeWithName:(NSString *)attrName {
    // This method is only used on the iPhone simulator, where normal extended attributes are broken.
    // See full explanation in the header file.
    
    // Split the file name into components. File name may have various format, but generally
    // structure is same:
    //
    // <name part>.<extension part> and <name part>.archived.<extension part>
    // or
    // <name part> and <name part>.archived
    //
    // So we want to search for the attrName in the components (ignoring the first array index).
    
    NSArray *components = [[self fileName] componentsSeparatedByString:@"."];
    
    // Watch out for file names without an extension
    
    for (NSUInteger i = 1; i < components.count; i++) {
        NSString *attr = components[i];
        
        if ([attrName isEqualToString:attr]) {
            return YES;
        }
    }
    
    return NO;
}

- (void)addExtensionAttributeWithName:(NSString *)attrName {
    // This method is only used on the iPhone simulator, where normal extended attributes are broken.
    // See full explanation in the header file.
    
    if ([attrName length] == 0) {
        return;
    }
    
    // Example:
    // attrName = "archived"
    //
    // "mylog.txt" -> "mylog.archived.txt"
    // "mylog"     -> "mylog.archived"
    
    NSArray *components = [[self fileName] componentsSeparatedByString:@"."];
    
    NSUInteger count = [components count];
    
    NSUInteger estimatedNewLength = [[self fileName] length] + [attrName length] + 1;
    NSMutableString *newFileName = [NSMutableString stringWithCapacity:estimatedNewLength];
    
    if (count > 0) {
        [newFileName appendString:components.firstObject];
    }
    
    NSString *lastExt = @"";
    
    NSUInteger i;
    
    for (i = 1; i < count; i++) {
        NSString *attr = components[i];
        
        if ([attr length] == 0) {
            continue;
        }
        
        if ([attrName isEqualToString:attr]) {
            // Extension attribute already exists in file name
            return;
        }
        
        if ([lastExt length] > 0) {
            [newFileName appendFormat:@".%@", lastExt];
        }
        
        lastExt = attr;
    }
    
    [newFileName appendFormat:@".%@", attrName];
    
    if ([lastExt length] > 0) {
        [newFileName appendFormat:@".%@", lastExt];
    }
    
    [self renameFile:newFileName];
}

- (void)removeExtensionAttributeWithName:(NSString *)attrName {
    // This method is only used on the iPhone simulator, where normal extended attributes are broken.
    // See full explanation in the header file.
    
    if ([attrName length] == 0) {
        return;
    }
    
    // Example:
    // attrName = "archived"
    //
    // "mylog.archived.txt" -> "mylog.txt"
    // "mylog.archived"     -> "mylog"
    
    NSArray *components = [[self fileName] componentsSeparatedByString:@"."];
    
    NSUInteger count = [components count];
    
    NSUInteger estimatedNewLength = [[self fileName] length];
    NSMutableString *newFileName = [NSMutableString stringWithCapacity:estimatedNewLength];
    
    if (count > 0) {
        [newFileName appendString:components.firstObject];
    }
    
    BOOL found = NO;
    
    NSUInteger i;
    
    for (i = 1; i < count; i++) {
        NSString *attr = components[i];
        
        if ([attrName isEqualToString:attr]) {
            found = YES;
        } else {
            [newFileName appendFormat:@".%@", attr];
        }
    }
    
    if (found) {
        [self renameFile:newFileName];
    }
}

#else /* if TARGET_IPHONE_SIMULATOR */

- (BOOL)hasExtendedAttributeWithName:(NSString *)attrName {
    const char *path = [filePath UTF8String];
    const char *name = [attrName UTF8String];
    
    ssize_t result = getxattr(path, name, NULL, 0, 0, 0);
    
    return (result >= 0);
}

- (void)addExtendedAttributeWithName:(NSString *)attrName {
    const char *path = [filePath UTF8String];
    const char *name = [attrName UTF8String];
    
    int result = setxattr(path, name, NULL, 0, 0, 0);
    
    if (result < 0) {
        NSLogError(@"LIFELogFileInfo: setxattr(%@, %@): error = %s",
                   attrName,
                   filePath,
                   strerror(errno));
    }
}

- (void)removeExtendedAttributeWithName:(NSString *)attrName {
    const char *path = [filePath UTF8String];
    const char *name = [attrName UTF8String];
    
    int result = removexattr(path, name, 0);
    
    if (result < 0 && errno != ENOATTR) {
        NSLogError(@"LIFELogFileInfo: removexattr(%@, %@): error = %s",
                   attrName,
                   self.fileName,
                   strerror(errno));
    }
}

#endif /* if TARGET_IPHONE_SIMULATOR */

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Comparisons
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (BOOL)isEqual:(id)object {
    if ([object isKindOfClass:[self class]]) {
        LIFELogFileInfo *another = (LIFELogFileInfo *)object;
        
        return [filePath isEqualToString:[another filePath]];
    }
    
    return NO;
}

- (NSComparisonResult)reverseCompareByCreationDate:(LIFELogFileInfo *)another {
    NSDate *us = [self creationDate];
    NSDate *them = [another creationDate];
    
    NSComparisonResult result = [us compare:them];
    
    if (result == NSOrderedAscending) {
        return NSOrderedDescending;
    }
    
    if (result == NSOrderedDescending) {
        return NSOrderedAscending;
    }
    
    return NSOrderedSame;
}

- (NSComparisonResult)reverseCompareByModificationDate:(LIFELogFileInfo *)another {
    NSDate *us = [self modificationDate];
    NSDate *them = [another modificationDate];
    
    NSComparisonResult result = [us compare:them];
    
    if (result == NSOrderedAscending) {
        return NSOrderedDescending;
    }
    
    if (result == NSOrderedDescending) {
        return NSOrderedAscending;
    }
    
    return NSOrderedSame;
}

@end

#if TARGET_OS_IPHONE
/**
 * When creating log file on iOS we're setting NSFileProtectionKey attribute to NSFileProtectionCompleteUnlessOpen.
 *
 * But in case if app is able to launch from background we need to have an ability to open log file any time we
 * want (even if device is locked). Thats why that attribute have to be changed to
 * NSFileProtectionCompleteUntilFirstUserAuthentication.
 */
BOOL life_doesAppRunInBackground() {
    BOOL answer = NO;
    
    NSArray *backgroundModes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIBackgroundModes"];
    
    for (NSString *mode in backgroundModes) {
        if (mode.length > 0) {
            answer = YES;
            break;
        }
    }
    
    return answer;
}

#endif

