#import "ThinkingExceptionHandler.h"
#include <libkern/OSAtomic.h>
#include <stdatomic.h>
#import "TDLogging.h"
#import "TDAutoTrackManager.h"

#if __has_include(<ThinkingDataCore/TDCorePresetDisableConfig.h>)
#import <ThinkingDataCore/TDCorePresetDisableConfig.h>
#else
#import "TDCorePresetDisableConfig.h"
#endif

static NSString * const TD_CRASH_REASON = @"#app_crashed_reason";
static NSUInteger const TD_PROPERTY_CRASH_LENGTH_LIMIT = 8191*2;

static NSString * const TDUncaughtExceptionHandlerSignalExceptionName = @"UncaughtExceptionHandlerSignalExceptionName";
static NSString * const TDUncaughtExceptionHandlerSignalKey = @"UncaughtExceptionHandlerSignalKey";
static int TDSignals[] = {SIGILL, SIGABRT, SIGBUS, SIGSEGV, SIGFPE, SIGPIPE, SIGTRAP};
static volatile atomic_int_fast32_t TDExceptionCount = 0;
static const atomic_int_fast32_t TDExceptionMaximum = 9;

@interface ThinkingExceptionHandler ()
@property (nonatomic) NSUncaughtExceptionHandler *td_lastExceptionHandler;
@property (nonatomic, unsafe_unretained) struct sigaction *td_signalHandlers;

@end

@implementation ThinkingExceptionHandler

+ (void)start {
    [self sharedHandler];
}

+ (instancetype)sharedHandler {
    static ThinkingExceptionHandler *gSharedHandler = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        gSharedHandler = [[ThinkingExceptionHandler alloc] init];
    });
    return gSharedHandler;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        _td_signalHandlers = calloc(NSIG, sizeof(struct sigaction));
        [self setupHandlers];
        
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
        // start APMStuck
        Class cls = NSClassFromString(@"TAAPMStuckMonitor");
        if (cls && [cls respondsToSelector:@selector(shareInstance)]) {
            id ins = [cls performSelector:@selector(shareInstance)];
            if (ins && [ins respondsToSelector:@selector(beginMonitor)]) {
                [ins performSelector:@selector(beginMonitor)];
            }
        }
#pragma clang diagnostic push
    }
    return self;
}

- (void)setupHandlers {
    _td_lastExceptionHandler = NSGetUncaughtExceptionHandler();
    NSSetUncaughtExceptionHandler(&TDHandleException);
    
    struct sigaction action;
    sigemptyset(&action.sa_mask);
    action.sa_flags = SA_SIGINFO;
    action.sa_sigaction = &TDSignalHandler;
    for (int i = 0; i < sizeof(TDSignals) / sizeof(int); i++) {
        struct sigaction prev_action;
        int err = sigaction(TDSignals[i], &action, &prev_action);
        if (err == 0) {
            memcpy(_td_signalHandlers + TDSignals[i], &prev_action, sizeof(prev_action));
        } else {
            TDLogError(@"Error Signal: %d", TDSignals[i]);
        }
    }
}

static void TDHandleException(NSException *exception) {
    ThinkingExceptionHandler *handler = [ThinkingExceptionHandler sharedHandler];

    atomic_int_fast32_t exceptionCount = atomic_fetch_add_explicit(&TDExceptionCount, 1, memory_order_relaxed);
    if (exceptionCount <= TDExceptionMaximum) {
        [handler td_handleUncaughtException:exception];
    }
    if (handler.td_lastExceptionHandler) {
        handler.td_lastExceptionHandler(exception);
    }
}

static void TDSignalHandler(int signalNumber, struct __siginfo *info, void *context) {
    ThinkingExceptionHandler *handler = [ThinkingExceptionHandler sharedHandler];
    NSMutableDictionary *crashInfo;
    NSString *reason;
    NSException *exception;
    
    atomic_int_fast32_t exceptionCount = atomic_fetch_add_explicit(&TDExceptionCount, 1, memory_order_relaxed);
    if (exceptionCount <= TDExceptionMaximum) {
        [crashInfo setObject:[NSNumber numberWithInt:signalNumber] forKey:TDUncaughtExceptionHandlerSignalKey];
        reason = [NSString stringWithFormat:@"Signal %d was raised.", signalNumber];
        exception = [NSException exceptionWithName:TDUncaughtExceptionHandlerSignalExceptionName reason:reason userInfo:crashInfo];
        [handler td_handleUncaughtException:exception];
    }
    
    struct sigaction prev_action = handler.td_signalHandlers[signalNumber];
    if (prev_action.sa_handler == SIG_DFL) {
        signal(signalNumber, SIG_DFL);
        raise(signalNumber);
        return;
    }
    if (prev_action.sa_flags & SA_SIGINFO) {
        if (prev_action.sa_sigaction) {
            prev_action.sa_sigaction(signalNumber, info, context);
        }
    } else if (prev_action.sa_handler) {
        prev_action.sa_handler(signalNumber);
    }
}

- (void)td_handleUncaughtException:(NSException *)exception {
    NSDictionary *crashInfo = [ThinkingExceptionHandler crashInfoWithException:exception];
    TDAutoTrackEvent *crashEvent = [[TDAutoTrackEvent alloc] initWithName:TD_APP_CRASH_EVENT];
    [[TDAutoTrackManager sharedManager] trackWithEvent:crashEvent withProperties:crashInfo];
    
    TDAutoTrackEvent *appEndEvent = [[TDAutoTrackEvent alloc] initWithName:TD_APP_END_EVENT];
    [[TDAutoTrackManager sharedManager] trackWithEvent:appEndEvent withProperties:nil];
    
    [[TDAutoTrackManager sharedManager] flush];
    dispatch_sync([ThinkingAnalyticsSDK sharedTrackQueue], ^{});
    
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    dispatch_queue_t networkQueue = [TDEventTracker td_networkQueue];
    dispatch_async(networkQueue, ^{
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)));
    
    NSSetUncaughtExceptionHandler(NULL);
    for (int i = 0; i < sizeof(TDSignals) / sizeof(int); i++) {
        signal(TDSignals[i], SIG_DFL);
    }
}

+ (void)trackCrashWithMessage:(NSString *)message {
    NSDictionary *crashInfo = [ThinkingExceptionHandler crashInfoWithMessage:message];
    TDAutoTrackEvent *crashEvent = [[TDAutoTrackEvent alloc] initWithName:TD_APP_CRASH_EVENT];
    [[TDAutoTrackManager sharedManager] trackWithEvent:crashEvent withProperties:crashInfo];
}

+ (NSMutableDictionary *)crashInfoWithMessage:(NSString *)message {
    NSMutableDictionary *properties = [[NSMutableDictionary alloc] init];
    
    if ([TDCorePresetDisableConfig disableAppCrashedReason]) {
        return properties;
    }
    
    NSString *crashStr = message;
    @try {
        crashStr = [crashStr stringByReplacingOccurrencesOfString:@"\n" withString:@"<br>"];

        NSUInteger strLength = [((NSString *)crashStr) lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
        NSUInteger strMaxLength = TD_PROPERTY_CRASH_LENGTH_LIMIT;
        if (strLength > strMaxLength) {
            crashStr = [NSMutableString stringWithString:[ThinkingExceptionHandler limitString:crashStr withLength:strMaxLength - 1]];
        }

        [properties setValue:crashStr forKey:TD_CRASH_REASON];
    } @catch(NSException *exception) {
        TDLogError(@"%@ error: %@", self, exception);
    }
    return properties;
}

+ (NSMutableDictionary *)crashInfoWithException:(NSException *)exception {
    NSMutableDictionary *properties = [[NSMutableDictionary alloc] init];
    
    
    if ([TDCorePresetDisableConfig disableAppCrashedReason]) {
        return properties;
    }
    
    NSString *crashStr;
    @try {
        if ([exception callStackSymbols]) {
            crashStr = [NSString stringWithFormat:@"Exception Reason:%@\nException Stack:%@", [exception reason], [exception callStackSymbols]];
        } else {
            NSString *exceptionStack = [[NSThread callStackSymbols] componentsJoinedByString:@"\n"];
            crashStr = [NSString stringWithFormat:@"%@ %@", [exception reason], exceptionStack];
        }
        crashStr = [crashStr stringByReplacingOccurrencesOfString:@"\n" withString:@"<br>"];

        NSUInteger strLength = [((NSString *)crashStr) lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
        NSUInteger strMaxLength = TD_PROPERTY_CRASH_LENGTH_LIMIT;
        if (strLength > strMaxLength) {
            crashStr = [NSMutableString stringWithString:[ThinkingExceptionHandler limitString:crashStr withLength:strMaxLength - 1]];
        }

        [properties setValue:crashStr forKey:TD_CRASH_REASON];
    } @catch(NSException *exception) {
        TDLogError(@"%@ error: %@", self, exception);
    }
    return properties;
}

+ (NSString *)limitString:(NSString *)originalString withLength:(NSInteger)length {
    NSStringEncoding encoding = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingUTF8);
    NSData *originalData = [originalString dataUsingEncoding:encoding];
    NSData *subData = [originalData subdataWithRange:NSMakeRange(0, length)];
    NSString *limitString = [[NSString alloc] initWithData:subData encoding:encoding];

    NSInteger index = 1;
    while (index <= 3 && !limitString) {
        if (length > index) {
            subData = [originalData subdataWithRange:NSMakeRange(0, length - index)];
            limitString = [[NSString alloc] initWithData:subData encoding:encoding];
        }
        index ++;
    }

    if (!limitString) {
        return originalString;
    }
    return limitString;
}

@end
