#import <dlfcn.h>
#import <objc/runtime.h>
#import <stdlib.h>
#import <stdio.h>
#import <unistd.h>
#import <pthread.h>
#import <sys/stat.h>
#import <sys/types.h>
#import <CommonCrypto/CommonDigest.h>
#include <syslog.h>

#define PROC_PIDPATHINFO_MAXSIZE  (1024)
int proc_pidpath(pid_t pid, void *buffer, uint32_t buffersize);

#define dylibDir @"/usr/lib/TweakInject"

NSArray *sbinjectGenerateDylibList() {

    NSString *processName = [[NSProcessInfo processInfo] processName];
    // launchctl, amfid you are special cases
    if ([processName isEqualToString:@"launchctl"]) {
        HBLogInfo(@"launchctl exit");
        return nil;
    }
    if ([processName isEqualToString:@"amfid"]) {
        HBLogInfo(@"amfid exit");
        return nil;
    }
    // Create an array containing all the filenames in dylibDir (/opt/simject)
    NSError *e = nil;
    NSArray *dylibDirContents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dylibDir error:&e];
    if (e) {
        return nil;
    }
    // Read current bundle identifier
    NSString *bundleIdentifier = NSBundle.mainBundle.bundleIdentifier;
    //NSLog(@"bundleID: %@", bundleIdentifier);
    // We're only interested in the plist files
    NSArray *plists = [dylibDirContents filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"SELF ENDSWITH %@", @"plist"]];
    // Create an empty mutable array that will contain a list of dylib paths to be injected into the target process
    NSMutableArray *dylibsToInject = [NSMutableArray array];
    // Loop through the list of plists
    for (NSString *plist in plists) {
        // We'll want to deal with absolute paths, so append the filename to dylibDir

        //NSLog(@"plist: %@", plist);
        NSString *plistPath = [dylibDir stringByAppendingPathComponent:plist];
        NSDictionary *filter = [NSDictionary dictionaryWithContentsOfFile:plistPath];
        // This boolean indicates whether or not the dylib has already been injected
        BOOL isInjected = NO;
        // If supported iOS versions are specified within the plist, we check those first
        NSArray *supportedVersions = filter[@"CoreFoundationVersion"];
        if (supportedVersions) {
            if (supportedVersions.count != 1 && supportedVersions.count != 2) {
                continue; // Supported versions are in the wrong format, we should skip
            }
            if (supportedVersions.count == 1 && [supportedVersions[0] doubleValue] > kCFCoreFoundationVersionNumber) {
                continue; // Doesn't meet lower bound
            }
            if (supportedVersions.count == 2 && ([supportedVersions[0] doubleValue] > kCFCoreFoundationVersionNumber || [supportedVersions[1] doubleValue] <= kCFCoreFoundationVersionNumber)) {
                continue; // Outside bounds
            }
        }
        // Decide whether or not to load the dylib based on the Bundles values
        
        NSArray *injectBundles = filter[@"Filter"][@"Bundles"];
        //NSLog(@"injectBundles: %@ vs %@", injectBundles, bundleIdentifier);

        if ([injectBundles containsObject:bundleIdentifier]){

            NSLog(@"inject bundles contains object: %@", bundleIdentifier);
            
            [dylibsToInject addObject:[[plistPath stringByDeletingPathExtension] stringByAppendingString:@".dylib"]];
            isInjected = YES;
            continue;
        }


            //this code never worked for me on tvOS
        for (NSString *entry in filter[@"Filter"][@"Bundles"]) {
            // Check to see whether or not this bundle is actually loaded in this application or not
            if (!CFBundleGetBundleWithIdentifier((CFStringRef)entry)) {
                // If not, skip it
                //NSLog(@"we not in here?: %@", entry);
                continue;
            }
            [dylibsToInject addObject:[[plistPath stringByDeletingPathExtension] stringByAppendingString:@".dylib"]];
            isInjected = YES;
            //NSLog(@"here?: %@", entry);
            continue;
        }
        
        if (!isInjected) {
            // Decide whether or not to load the dylib based on the Executables values
            for (NSString *process in filter[@"Filter"][@"Executables"]) {
                if ([process isEqualToString:processName]) {
                    [dylibsToInject addObject:[[plistPath stringByDeletingPathExtension] stringByAppendingString:@".dylib"]];
                    isInjected = YES;
                    continue;
                }
            }
        }
        if (!isInjected) {
            // Decide whether or not to load the dylib based on the Classes values
            for (NSString *clazz in filter[@"Filter"][@"Classes"]) {
                // Also check if this class is loaded in this application or not
                if (!NSClassFromString(clazz)) {
                    // This class couldn't be loaded, skip
                    continue;
                }
                // It's fine to add this dylib at this point
                [dylibsToInject addObject:[[plistPath stringByDeletingPathExtension] stringByAppendingString:@".dylib"]];
                isInjected = YES;
                continue;
            }
        }
    }

    [dylibsToInject sortUsingSelector:@selector(caseInsensitiveCompare:)];
    return dylibsToInject;
}

int file_exist(char *filename) {
    struct stat buffer;
    int r = stat(filename, &buffer);
    return (r == 0);
}



@interface SpringBoard : NSObject
- (BOOL)launchApplicationWithIdentifier:(NSString *)identifier suspended:(BOOL)suspended;
- (id)sharedApplication;
@end

%group SafeMode
%hook FBApplicationInfo
- (NSDictionary *)environmentVariables {
    NSDictionary *originalVariables = %orig;
    NSMutableDictionary *newVariables = [originalVariables mutableCopy];
    [newVariables setObject:@1 forKey:@"_SafeMode"];
    return [newVariables autorelease];
}
%end
/*
%hook SBLockScreenManager
-(BOOL)_finishUIUnlockFromSource:(int)arg1 withOptions:(id)arg2 {
    BOOL ret = %orig;
    [(SpringBoard *)[%c(UIApplication) sharedApplication] launchApplicationWithIdentifier:@"org.coolstar.SafeMode" suspended:NO];
    return ret;
}

// Necessary on iPhone X to show after swipe unlock gesture
-(void)lockScreenViewControllerDidDismiss {
    %orig;
    [(SpringBoard *)[%c(UIApplication) sharedApplication] launchApplicationWithIdentifier:@"org.coolstar.SafeMode" suspended:NO];
}
%end
*/
%end

static BOOL isSpringBoardOrBackboard = NO;
static NSString *processHash = @"";
BOOL safeMode = false;

void SpringBoardSigHandler(int signo, siginfo_t *info, void *uap){
    if (isSpringBoardOrBackboard){
        FILE *f = fopen("/var/mobile/Library/.sbinjectSafeMode", "w");
        fprintf(f, "Hello World\n");
        fclose(f);
    }
    FILE *f = fopen([[NSString stringWithFormat:@"%@/.safeMode-%@", NSTemporaryDirectory(), processHash] UTF8String], "w");
    fprintf(f, "Hello World!\n");
    fclose(f);

    raise(signo);
}

__attribute__ ((constructor))
static void ctor(void) {
    @autoreleasepool {
        if (NSBundle.mainBundle.bundleIdentifier == nil || ![NSBundle.mainBundle.bundleIdentifier isEqualToString:@"org.coolstar.SafeMode"]){
            char pathbuf[PROC_PIDPATHINFO_MAXSIZE] = {0};
            int ret = proc_pidpath(getpid(), pathbuf, sizeof(pathbuf));
            if (ret > 0){
                uint8_t digest[CC_SHA1_DIGEST_LENGTH];

                CC_SHA1(pathbuf, ret, digest);

                NSMutableString *output = [NSMutableString stringWithCapacity:CC_SHA1_DIGEST_LENGTH * 2];

                for (int i = 0; i < CC_SHA1_DIGEST_LENGTH; i++)
                {
                    [output appendFormat:@"%02x", digest[i]];
                }
                processHash = [[NSString alloc] initWithString:output];
            }

            safeMode = false;
            NSString *processName = [[NSProcessInfo processInfo] processName];

            struct sigaction action;
            memset(&action, 0, sizeof(action));
            action.sa_sigaction = &SpringBoardSigHandler;
            action.sa_flags = SA_SIGINFO | SA_RESETHAND;
            sigemptyset(&action.sa_mask);

            sigaction(SIGQUIT, &action, NULL);
            sigaction(SIGILL, &action, NULL);
            sigaction(SIGTRAP, &action, NULL);
            sigaction(SIGABRT, &action, NULL);
            sigaction(SIGEMT, &action, NULL);
            sigaction(SIGFPE, &action, NULL);
            sigaction(SIGBUS, &action, NULL);
            sigaction(SIGSEGV, &action, NULL);
            sigaction(SIGSYS, &action, NULL);

            if ([processName isEqualToString:@"backboardd"] || [NSBundle.mainBundle.bundleIdentifier isEqualToString:@"com.apple.springboard"]){
                isSpringBoardOrBackboard = YES;
                if (file_exist("/var/mobile/Library/.sbinjectSafeMode")){
                    safeMode = true;
                    if ([NSBundle.mainBundle.bundleIdentifier isEqualToString:@"com.apple.springboard"]){
                        unlink("/var/mobile/Library/.sbinjectSafeMode");
                        NSLog(@"Entering Safe Mode!");
                        %init(SafeMode);
                    }
                }
            }

            if ([NSBundle.mainBundle.bundleIdentifier isEqualToString:@"com.apple.springboard"]){
                dlopen("/usr/lib/TweakInjectMapsCheck.dylib", RTLD_LAZY | RTLD_GLOBAL);
            }

            const char *safeModeByProcPath = [[NSString stringWithFormat:@"%@/.safeMode-%@", NSTemporaryDirectory(), processHash] UTF8String];
            if (file_exist((char *)safeModeByProcPath)){
                safeMode = true;
                unlink(safeModeByProcPath);
            }

            if (getenv("_MSSafeMode")){
                if (strcmp(getenv("_MSSafeMode"),"1") == 0){
                    safeMode = true;
                }
            }
            if (getenv("_SafeMode")){
                if (strcmp(getenv("_SafeMode"),"1") == 0){
                    safeMode = true;
                }
            }
            if (getenv("_SubstituteSafeMode")){
                if (strcmp(getenv("_SubstituteSafeMode"),"1") == 0){
                    safeMode = true;
                }
            }

            if (!safeMode){

                //HBLogInfo(@"In bundle: %@", NSBundle.mainBundle.bundleIdentifier);

                NSArray *theList = sbinjectGenerateDylibList();
                //HBLogInfo(@"theList: %@", theList);
                for (NSString *dylib in theList) {
                    NSLog(@"Injecting %@", dylib);
                    void *dl = dlopen([dylib UTF8String], RTLD_LAZY | RTLD_GLOBAL);

                    if (dl == NULL) {
                        NSLog(@"Injection failed: '%s'", dlerror());
                    }
                }
            } else {
                NSLog(@"TweakInject: Entering Safe Mode!");
            }
        }
    }
}
