//
//  SentryCrashDoctor.m
//  SentryCrash
//
//  Created by Karl Stenerud on 2012-11-10.
//  Copyright (c) 2012 Karl Stenerud. All rights reserved.
//

#import "SentryCrashDoctor.h"
#import "SentryCrashReportFields.h"
#import "SentryCrashMonitor_System.h"


typedef enum
{
    CPUFamilyUnknown,
    CPUFamilyArm,
    CPUFamilyX86,
    CPUFamilyX86_64
} CPUFamily;

@interface  SentryCrashDoctorParam: NSObject

@property(nonatomic, readwrite, retain) NSString* className;
@property(nonatomic, readwrite, retain) NSString* previousClassName;
@property(nonatomic, readwrite, retain) NSString* type;
@property(nonatomic, readwrite, assign) BOOL isInstance;
@property(nonatomic, readwrite, assign) uintptr_t address;
@property(nonatomic, readwrite, retain) NSString* value;

@end

@implementation SentryCrashDoctorParam

@synthesize className = _className;
@synthesize previousClassName = _previousClassName;
@synthesize isInstance = _isInstance;
@synthesize address = _address;
@synthesize value = _value;
@synthesize type = _type;

@end

@interface SentryCrashDoctorFunctionCall: NSObject

@property(nonatomic, readwrite, retain) NSString* name;
@property(nonatomic, readwrite, retain) NSArray* params;

@end

@implementation SentryCrashDoctorFunctionCall

@synthesize name = _name;
@synthesize params = _params;

- (NSString*) descriptionForObjCCall
{
    if(![self.name isEqualToString:@"objc_msgSend"])
    {
        return nil;
    }
    SentryCrashDoctorParam* receiverParam = [self.params objectAtIndex:0];
    NSString* receiver = receiverParam.previousClassName;
    if(receiver == nil)
    {
        receiver = receiverParam.className;
        if(receiver == nil)
        {
            receiver = @"id";
        }
    }

    SentryCrashDoctorParam* selectorParam = [self.params objectAtIndex:1];
    if(![selectorParam.type isEqualToString:@SentryCrashMemType_String])
    {
        return nil;
    }
    NSArray* splitSelector = [selectorParam.value componentsSeparatedByString:@":"];
    int paramCount = (int)splitSelector.count - 1;

    NSMutableString* string = [NSMutableString stringWithFormat:@"-[%@ %@", receiver, [splitSelector objectAtIndex:0]];
    for(int paramNum = 0; paramNum < paramCount; paramNum++)
    {
        [string appendString:@":"];
        if(paramNum < 2)
        {
            SentryCrashDoctorParam* param = [self.params objectAtIndex:(NSUInteger)paramNum + 2];
            if(param.value != nil)
            {
                if([param.type isEqualToString:@SentryCrashMemType_String])
                {
                    [string appendFormat:@"\"%@\"", param.value];
                }
                else
                {
                    [string appendString:param.value];
                }
            }
            else if(param.previousClassName != nil)
            {
                [string appendString:param.previousClassName];
            }
            else if(param.className != nil)
            {
                [string appendFormat:@"%@ (%@)", param.className, param.isInstance ? @"instance" : @"class"];
            }
            else
            {
                [string appendString:@"?"];
            }
        }
        else
        {
            [string appendString:@"?"];
        }
        if(paramNum < paramCount - 1)
        {
            [string appendString:@" "];
        }
    }

    [string appendString:@"]"];
    return string;
}

- (NSString*) descriptionWithParamCount:(int) paramCount
{
    NSString* objCCall = [self descriptionForObjCCall];
    if(objCCall != nil)
    {
        return objCCall;
    }

    if(paramCount > (int)self.params.count)
    {
        paramCount = (int)self.params.count;
    }
    NSMutableString* str = [NSMutableString string];
    [str appendFormat:@"Function: %@\n", self.name];
    for(int i = 0; i < paramCount; i++)
    {
        SentryCrashDoctorParam* param = [self.params objectAtIndex:(NSUInteger)i];
        [str appendFormat:@"Param %d:  ", i + 1];
        if(param.className != nil)
        {
            [str appendFormat:@"%@ (%@) ", param.className, param.isInstance ? @"instance" : @"class"];
        }
        if(param.value != nil)
        {
            [str appendFormat:@"%@ ", param.value];
        }
        if(param.previousClassName != nil)
        {
            [str appendFormat:@"(was %@)", param.previousClassName];
        }
        if(i < paramCount - 1)
        {
            [str appendString:@"\n"];
        }
    }
    return str;
}

@end



@implementation SentryCrashDoctor

+ (SentryCrashDoctor*) doctor
{
    return [[self alloc] init];
}

- (NSDictionary*) recrashReport:(NSDictionary*) report
{
    return [report objectForKey:@SentryCrashField_RecrashReport];
}

- (NSDictionary*) systemReport:(NSDictionary*) report
{
    return [report objectForKey:@SentryCrashField_System];
}

- (NSDictionary*) crashReport:(NSDictionary*) report
{
    return [report objectForKey:@SentryCrashField_Crash];
}

- (NSDictionary*) infoReport:(NSDictionary*) report
{
    return [report objectForKey:@SentryCrashField_Report];
}

- (NSDictionary*) errorReport:(NSDictionary*) report
{
    return [[self crashReport:report] objectForKey:@SentryCrashField_Error];
}

- (CPUFamily) cpuFamily:(NSDictionary*) report
{
    NSDictionary* system = [self systemReport:report];
    NSString* cpuArch = [system objectForKey:@SentryCrashField_CPUArch];
    if([cpuArch rangeOfString:@"arm"].location == 0)
    {
        return CPUFamilyArm;
    }
    if([cpuArch rangeOfString:@"i"].location == 0 && [cpuArch rangeOfString:@"86"].location == 2)
    {
        return CPUFamilyX86;
    }
    if ([cpuArch rangeOfString:@"x86_64" options:NSCaseInsensitiveSearch].location != NSNotFound) {
        return CPUFamilyX86_64;
    }
    return CPUFamilyUnknown;
}

- (NSString*) registerNameForFamily:(CPUFamily) family paramIndex:(int) index
{
    switch (family)
    {
        case CPUFamilyArm:
        {
            switch(index)
            {
                case 0:
                    return @"r0";
                case 1:
                    return @"r1";
                case 2:
                    return @"r2";
                case 3:
                    return @"r3";
            }
        }
        case CPUFamilyX86:
        {
            switch(index)
            {
                case 0:
                    return @"edi";
                case 1:
                    return @"esi";
                case 2:
                    return @"edx";
                case 3:
                    return @"ecx";
            }
        }
        case CPUFamilyX86_64:
        {
            switch(index)
            {
                case 0:
                    return @"rdi";
                case 1:
                    return @"rsi";
                case 2:
                    return @"rdx";
                case 3:
                    return @"rcx";
            }
        }
        case CPUFamilyUnknown:
            return nil;
    }
    return nil;
}

- (NSString*) mainExecutableNameForReport:(NSDictionary*) report
{
    NSDictionary* info = [self infoReport:report];
    return [info objectForKey:@SentryCrashField_ProcessName];
}

- (NSDictionary*) crashedThreadReport:(NSDictionary*) report
{
    NSDictionary* crashReport = [self crashReport:report];
    NSDictionary* crashedThread = [crashReport objectForKey:@SentryCrashField_CrashedThread];
    if(crashedThread != nil)
    {
        return crashedThread;
    }

    for(NSDictionary* thread in [crashReport objectForKey:@SentryCrashField_Threads])
    {
        if([[thread objectForKey:@SentryCrashField_Crashed] boolValue])
        {
            return thread;
        }
    }
    return nil;
}

- (NSArray*) backtraceFromThreadReport:(NSDictionary*) threadReport
{
    NSDictionary* backtrace = [threadReport objectForKey:@SentryCrashField_Backtrace];
    return [backtrace objectForKey:@SentryCrashField_Contents];
}

- (NSDictionary*) basicRegistersFromThreadReport:(NSDictionary*) threadReport
{
    NSDictionary* registers = [threadReport objectForKey:@SentryCrashField_Registers];
    NSDictionary* basic = [registers objectForKey:@SentryCrashField_Basic];
    return basic;
}

- (NSDictionary*) lastInAppStackEntry:(NSDictionary*) report
{
    NSString* executableName = [self mainExecutableNameForReport:report];
    NSDictionary* crashedThread = [self crashedThreadReport:report];
    NSArray* backtrace = [self backtraceFromThreadReport:crashedThread];
    for(NSDictionary* entry in backtrace)
    {
        NSString* objectName = [entry objectForKey:@SentryCrashField_ObjectName];
        if([objectName isEqualToString:executableName])
        {
            return entry;
        }
    }
    return nil;
}

- (NSDictionary*) lastStackEntry:(NSDictionary*) report
{
    NSDictionary* crashedThread = [self crashedThreadReport:report];
    NSArray* backtrace = [self backtraceFromThreadReport:crashedThread];
    if([backtrace count] > 0)
    {
        return [backtrace objectAtIndex:0];
    }
    return nil;
}

- (BOOL) isInvalidAddress:(NSDictionary*) errorReport
{
    NSDictionary* machError = [errorReport objectForKey:@SentryCrashField_Mach];
    if(machError != nil)
    {
        NSString* exceptionName = [machError objectForKey:@SentryCrashField_ExceptionName];
        return [exceptionName isEqualToString:@"EXC_BAD_ACCESS"];
    }
    NSDictionary* signal = [errorReport objectForKey:@SentryCrashField_Signal];
    NSString* sigName = [signal objectForKey:@SentryCrashField_Name];
    return [sigName isEqualToString:@"SIGSEGV"];
}

- (BOOL) isMathError:(NSDictionary*) errorReport
{
    NSDictionary* machError = [errorReport objectForKey:@SentryCrashField_Mach];
    if(machError != nil)
    {
        NSString* exceptionName = [machError objectForKey:@SentryCrashField_ExceptionName];
        return [exceptionName isEqualToString:@"EXC_ARITHMETIC"];
    }
    NSDictionary* signal = [errorReport objectForKey:@SentryCrashField_Signal];
    NSString* sigName = [signal objectForKey:@SentryCrashField_Name];
    return [sigName isEqualToString:@"SIGFPE"];
}

- (BOOL) isMemoryCorruption:(NSDictionary*) report
{
    NSDictionary* crashedThread = [self crashedThreadReport:report];
    NSArray* notableAddresses = [crashedThread objectForKey:@SentryCrashField_NotableAddresses];
    for(NSDictionary* address in [notableAddresses objectEnumerator])
    {
        NSString* type = [address objectForKey:@SentryCrashField_Type];
        if([type isEqualToString:@"string"])
        {
            NSString* value = [address objectForKey:@SentryCrashField_Value];
            if([value rangeOfString:@"autorelease pool page"].location != NSNotFound &&
               [value rangeOfString:@"corrupted"].location != NSNotFound)
            {
                return YES;
            }
            if([value rangeOfString:@"incorrect checksum for freed object"].location != NSNotFound)
            {
                return YES;
            }
        }
    }

    NSArray* backtrace = [self backtraceFromThreadReport:crashedThread];
    for(NSDictionary* entry in backtrace)
    {
        NSString* objectName = [entry objectForKey:@SentryCrashField_ObjectName];
        NSString* symbolName = [entry objectForKey:@SentryCrashField_SymbolName];
        if([symbolName isEqualToString:@"objc_autoreleasePoolPush"])
        {
            return YES;
        }
        if([symbolName isEqualToString:@"free_list_checksum_botch"])
        {
            return YES;
        }
        if([symbolName isEqualToString:@"szone_malloc_should_clear"])
        {
            return YES;
        }
        if([symbolName isEqualToString:@"lookUpMethod"] && [objectName isEqualToString:@"libobjc.A.dylib"])
        {
            return YES;
        }
    }

    return NO;
}

- (SentryCrashDoctorFunctionCall*) lastFunctionCall:(NSDictionary*) report
{
    SentryCrashDoctorFunctionCall* function = [[SentryCrashDoctorFunctionCall alloc] init];
    NSDictionary* lastStackEntry = [self lastStackEntry:report];
    function.name = [lastStackEntry objectForKey:@SentryCrashField_SymbolName];

    NSDictionary* crashedThread = [self crashedThreadReport:report];
    NSDictionary* notableAddresses = [crashedThread objectForKey:@SentryCrashField_NotableAddresses];
    CPUFamily family = [self cpuFamily:report];
    NSDictionary* registers = [self basicRegistersFromThreadReport:crashedThread];
    NSArray* regNames = [NSArray arrayWithObjects:
                           [self registerNameForFamily:family paramIndex:0],
                           [self registerNameForFamily:family paramIndex:1],
                           [self registerNameForFamily:family paramIndex:2],
                           [self registerNameForFamily:family paramIndex:3],
                           nil];
    NSMutableArray* params = [NSMutableArray arrayWithCapacity:4];
    for(NSString* regName in regNames)
    {
        SentryCrashDoctorParam* param = [[SentryCrashDoctorParam alloc] init];
        param.address = (uintptr_t)[[registers objectForKey:regName] unsignedLongLongValue];
        NSDictionary* notableAddress = [notableAddresses objectForKey:regName];
        if(notableAddress == nil)
        {
            param.value = [NSString stringWithFormat:@"%p", (void*)param.address];
        }
        else
        {
            param.type = [notableAddress objectForKey:@SentryCrashField_Type];
            NSString* className = [notableAddress objectForKey:@SentryCrashField_Class];
            NSString* previousClass = [notableAddress objectForKey:@SentryCrashField_LastDeallocObject];
            NSString* value = [notableAddress objectForKey:@SentryCrashField_Value];

            if([param.type isEqualToString:@SentryCrashMemType_String])
            {
                param.value = value;
            }
            else if([param.type isEqualToString:@SentryCrashMemType_Object])
            {
                param.className = className;
                param.isInstance = YES;
            }
            else if([param.type isEqualToString:@SentryCrashMemType_Class])
            {
                param.className = className;
                param.isInstance = NO;
            }
            param.previousClassName = previousClass;
        }

        [params addObject:param];
    }

    function.params = params;
    return function;
}

- (NSString*) zombieCall:(SentryCrashDoctorFunctionCall*) functionCall
{
    if([functionCall.name isEqualToString:@"objc_msgSend"] && functionCall.params.count > 0 && [[functionCall.params objectAtIndex:0] previousClassName] != nil)
    {
        return [functionCall descriptionWithParamCount:4];
    }
    else if([functionCall.name isEqualToString:@"objc_retain"] && functionCall.params.count > 0 && [[functionCall.params objectAtIndex:0] previousClassName] != nil)
    {
        return [functionCall descriptionWithParamCount:1];
    }
    return nil;
}

- (BOOL) isStackOverflow:(NSDictionary*) crashedThreadReport
{
    NSDictionary* stack = [crashedThreadReport objectForKey:@SentryCrashField_Stack];
    return [[stack objectForKey:@SentryCrashField_Overflow] boolValue];
}

- (BOOL) isDeadlock:(NSDictionary*) report
{
    NSDictionary* errorReport = [self errorReport:report];
    NSString* crashType = [errorReport objectForKey:@SentryCrashField_Type];
    return [@SentryCrashExcType_Deadlock isEqualToString:crashType];
}

- (NSString*) diagnoseCrash:(NSDictionary*) report
{
    @try
    {
        NSString* lastFunctionName = [[self lastInAppStackEntry:report] objectForKey:@SentryCrashField_SymbolName];
        NSDictionary* crashedThreadReport = [self crashedThreadReport:report];
        NSDictionary* errorReport = [self errorReport:report];

        if([self isDeadlock:report])
        {
            return [NSString stringWithFormat:@"Main thread deadlocked in %@", lastFunctionName];
        }

        if([self isStackOverflow:crashedThreadReport])
        {
            return [NSString stringWithFormat:@"Stack overflow in %@", lastFunctionName];
        }

        NSString* crashType = [errorReport objectForKey:@SentryCrashField_Type];
        if([crashType isEqualToString:@SentryCrashExcType_NSException])
        {
            NSDictionary* exception = [errorReport objectForKey:@SentryCrashField_NSException];
            NSString* name = [exception objectForKey:@SentryCrashField_Name];
            NSString* reason = [exception objectForKey:@SentryCrashField_Reason]? [exception objectForKey:@SentryCrashField_Reason]:[errorReport objectForKey:@SentryCrashField_Reason];
            return [NSString stringWithFormat:@"Application threw exception %@: %@", name, reason];
        }

        if([self isMemoryCorruption:report])
        {
            return @"Rogue memory write has corrupted memory.";
        }

        if([self isMathError:errorReport])
        {
            return @"Math error (usually caused from division by 0).";
        }

        SentryCrashDoctorFunctionCall* functionCall = [self lastFunctionCall:report];
        NSString* zombieCall = [self zombieCall:functionCall];
        if(zombieCall != nil)
        {
            return [NSString stringWithFormat:@"Possible zombie in call: %@", zombieCall];
        }

        if([self isInvalidAddress:errorReport])
        {
            uintptr_t address = (uintptr_t)[[errorReport objectForKey:@SentryCrashField_Address] unsignedLongLongValue];
            if(address == 0)
            {
                return @"Attempted to dereference null pointer.";
            }
            return [NSString stringWithFormat:@"Attempted to dereference garbage pointer %p.", (void*)address];
        }

        return nil;
    }
    @catch (NSException* e)
    {
        NSArray* symbols = [e callStackSymbols];
        if(symbols)
        {
            return [NSString stringWithFormat:@"No diagnosis due to exception %@:\n%@\nPlease file a bug report to the SentryCrash project.", e, symbols];
        }
        return [NSString stringWithFormat:@"No diagnosis due to exception %@\nPlease file a bug report to the SentryCrash project.", e];
    }
}

@end
