#import "RCTPushy.h"
#import "RCTPushyDownloader.h"
#import "RCTPushyManager.h"

#if __has_include("RCTReloadCommand.h")
#import "RCTReloadCommand.h"
#endif
// Thanks to this guard, we won't import this header when we build for the old architecture.
#ifdef RCT_NEW_ARCH_ENABLED
#import "RCTPushySpec.h"
#endif

#import <React/RCTConvert.h>
#import <React/RCTLog.h>
// #import <React/RCTReloadCommand.h>

static NSString *const keyPushyInfo = @"REACTNATIVECN_PUSHY_INFO_KEY";
static NSString *const paramPackageVersion = @"packageVersion";
static NSString *const paramBuildTime = @"buildTime";
static NSString *const paramLastVersion = @"lastVersion";
static NSString *const paramCurrentVersion = @"currentVersion";
static NSString *const paramIsFirstTime = @"isFirstTime";
static NSString *const paramIsFirstLoadOk = @"isFirstLoadOK";
static NSString *const keyUuid = @"REACTNATIVECN_PUSHY_UUID";
static NSString *const keyHashInfo = @"REACTNATIVECN_PUSHY_HASH_";
static NSString *const keyFirstLoadMarked = @"REACTNATIVECN_PUSHY_FIRSTLOADMARKED_KEY";
static NSString *const keyRolledBackMarked = @"REACTNATIVECN_PUSHY_ROLLEDBACKMARKED_KEY";
static NSString *const KeyPackageUpdatedMarked = @"REACTNATIVECN_PUSHY_ISPACKAGEUPDATEDMARKED_KEY";

// app info
static NSString * const AppVersionKey = @"appVersion";
static NSString * const BuildVersionKey = @"buildVersion";

// file def
static NSString * const BUNDLE_FILE_NAME = @"index.bundlejs";
static NSString * const SOURCE_PATCH_NAME = @"__diff.json";
static NSString * const BUNDLE_PATCH_NAME = @"index.bundlejs.patch";

// error def
static NSString * const ERROR_OPTIONS = @"options error";
static NSString * const ERROR_HDIFFPATCH = @"hdiffpatch error";
static NSString * const ERROR_FILE_OPERATION = @"file operation error";

// event def
static NSString * const EVENT_PROGRESS_DOWNLOAD = @"RCTPushyDownloadProgress";
// static NSString * const EVENT_PROGRESS_UNZIP = @"RCTPushyUnzipProgress";
static NSString * const PARAM_PROGRESS_HASH = @"hash";
static NSString * const PARAM_PROGRESS_RECEIVED = @"received";
static NSString * const PARAM_PROGRESS_TOTAL = @"total";


typedef NS_ENUM(NSInteger, PushyType) {
    PushyTypeFullDownload = 1,
    PushyTypePatchFromPackage = 2,
    PushyTypePatchFromPpk = 3,
    //TASK_TYPE_PLAIN_DOWNLOAD=4?
};

static BOOL ignoreRollback = false;

@implementation RCTPushy {
    RCTPushyManager *_fileManager;
    bool hasListeners;
}

@synthesize methodQueue = _methodQueue;

RCT_EXPORT_MODULE(RCTPushy);

+ (NSURL *)bundleURL
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    
    // Check for version changes first 
    NSString *curPackageVersion = [RCTPushy packageVersion];
    NSString *curBuildTime = [RCTPushy buildTime];
    NSString *storedPackageVersion = [defaults stringForKey:paramPackageVersion];
    NSString *storedBuildTime = [defaults stringForKey:paramBuildTime];
    
    // If stored versions don't exist, write current versions first
    if (!storedPackageVersion || !storedBuildTime) {
        [defaults setObject:curPackageVersion forKey:paramPackageVersion];
        [defaults setObject:curBuildTime forKey:paramBuildTime];
        storedPackageVersion = curPackageVersion;
        storedBuildTime = curBuildTime;
    }
    
    BOOL packageVersionChanged = ![curPackageVersion isEqualToString:storedPackageVersion];
    BOOL buildTimeChanged = ![curBuildTime isEqualToString:storedBuildTime];
    
    if (packageVersionChanged || buildTimeChanged) {
        // Clear all update data and store new versions
        [defaults setObject:nil forKey:keyPushyInfo];
        [defaults setObject:nil forKey:keyHashInfo];
        [defaults setObject:@(YES) forKey:KeyPackageUpdatedMarked];
        [defaults setObject:curPackageVersion forKey:paramPackageVersion];
        [defaults setObject:curBuildTime forKey:paramBuildTime];
        
        // ...need clear files later
    }
    
    NSDictionary *pushyInfo = [defaults dictionaryForKey:keyPushyInfo];
    if (pushyInfo) {
            NSString *curVersion = pushyInfo[paramCurrentVersion];
            
            BOOL isFirstTime = [pushyInfo[paramIsFirstTime] boolValue];
            BOOL isFirstLoadOK = [pushyInfo[paramIsFirstLoadOk] boolValue];
            
            NSString *loadVersion = curVersion;
            BOOL needRollback = (!ignoreRollback && isFirstTime == NO && isFirstLoadOK == NO) || loadVersion.length<=0;
            if (needRollback) {
                loadVersion = [self rollback];
            } else if (isFirstTime && !ignoreRollback){
                // bundleURL may be called many times, ignore rollbacks before process restarted again.
                ignoreRollback = true;

                NSMutableDictionary *newInfo = [[NSMutableDictionary alloc] initWithDictionary:pushyInfo];
                newInfo[paramIsFirstTime] = @(NO);
                [defaults setObject:newInfo forKey:keyPushyInfo];
                [defaults setObject:@(YES) forKey:keyFirstLoadMarked];
                
            }
            
            NSString *downloadDir = [RCTPushy downloadDir];
            while (loadVersion.length) {
                NSString *bundlePath = [[downloadDir stringByAppendingPathComponent:loadVersion] stringByAppendingPathComponent:BUNDLE_FILE_NAME];
                if ([[NSFileManager defaultManager] fileExistsAtPath:bundlePath isDirectory:NULL]) {
                    NSURL *bundleURL = [NSURL fileURLWithPath:bundlePath];
                    return bundleURL;
                } else {
                    RCTLogError(@"RCTPushy -- bundle version %@ not found", loadVersion);
                    loadVersion = [self rollback];
                }
            }
    }
    
    return [RCTPushy binaryBundleURL];
}

+ (NSString *) rollback {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    
    NSDictionary *pushyInfo = [defaults dictionaryForKey:keyPushyInfo];
    NSString *lastVersion = pushyInfo[paramLastVersion];
    NSString *curVersion = pushyInfo[paramCurrentVersion]; 
    if (lastVersion.length) {
        // roll back to last version
        [defaults setObject:@{paramCurrentVersion:lastVersion,
                              paramIsFirstTime:@(NO),
                              paramIsFirstLoadOk:@(YES)}
                     forKey:keyPushyInfo];
    }
    else {
        // roll back to bundle
        [defaults setObject:nil forKey:keyPushyInfo];
    }
    [defaults setObject:curVersion forKey:keyRolledBackMarked];
    
    return lastVersion;
}

+ (BOOL)requiresMainQueueSetup {
    // only set to YES if your module initialization relies on calling UIKit!
	return NO;
}

- (NSDictionary *)constantsToExport
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    
    NSMutableDictionary *ret = [NSMutableDictionary new];
    ret[@"downloadRootDir"] = [RCTPushy downloadDir];
    ret[@"packageVersion"] = [RCTPushy packageVersion];
    ret[@"buildTime"] = [RCTPushy buildTime];
    ret[@"rolledBackVersion"] = [defaults objectForKey:keyRolledBackMarked];
    ret[@"isFirstTime"] = [defaults objectForKey:keyFirstLoadMarked];
    ret[@"uuid"] = [defaults objectForKey:keyUuid];
    NSDictionary *pushyInfo = [defaults dictionaryForKey:keyPushyInfo];
    NSString *currentVersion = [pushyInfo objectForKey:paramCurrentVersion];
    ret[@"currentVersion"] = currentVersion;
    if (currentVersion != nil) {
        ret[@"currentVersionInfo"] = [defaults objectForKey:[keyHashInfo stringByAppendingString:currentVersion]];
    }
    
    // clear isFirstTimemarked
    if (ret[@"isFirstTime"]) {
        [defaults setObject:nil forKey:keyFirstLoadMarked];
    }
    
    // clear rolledbackmark
    if (ret[@"rolledBackVersion"] != nil) {
        [defaults setObject:nil forKey:keyRolledBackMarked];
        [self clearInvalidFiles];
    }
    
    // clear packageupdatemarked
    if ([[defaults objectForKey:KeyPackageUpdatedMarked] boolValue]) {
        [defaults setObject:nil forKey:KeyPackageUpdatedMarked];
        [self clearInvalidFiles];
    }
    

    return ret;
}

- (instancetype)init
{
    self = [super init];
    if (self) {
        _fileManager = [RCTPushyManager new];
    }
    return self;
}

RCT_EXPORT_METHOD(setUuid:(NSString *)uuid  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
    @try {
        NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
        [defaults setObject:uuid forKey:keyUuid];
        
        resolve(@true);
    }
    @catch (NSException *exception) {
        reject(@"json格式校验报错", nil, nil);
    }
}

RCT_EXPORT_METHOD(setLocalHashInfo:(NSString *)hash
                  value:(NSString *)value resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
    NSData *data = [value dataUsingEncoding:NSUTF8StringEncoding];
    NSError *error = nil;
    id object = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
    if (object && [object isKindOfClass:[NSDictionary class]]) {
        NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
        [defaults setObject:value forKey:[keyHashInfo stringByAppendingString:hash]];
        
        resolve(@true);
    } else {
        reject(@"json格式校验报错", nil, nil);
    }
}


RCT_EXPORT_METHOD(getLocalHashInfo:(NSString *)hash
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
    
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    resolve([defaults stringForKey:[keyHashInfo stringByAppendingString:hash]]);
}

RCT_EXPORT_METHOD(downloadFullUpdate:(NSDictionary *)options
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
    [self doPushy:PushyTypeFullDownload options:options callback:^(NSError *error) {
        if (error) {
            reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error);
        }
        else {
            resolve(nil);
        }
    }];
}

RCT_EXPORT_METHOD(downloadPatchFromPackage:(NSDictionary *)options
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
    [self doPushy:PushyTypePatchFromPackage options:options callback:^(NSError *error) {
        if (error) {
            reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error);
        }
        else {
            resolve(nil);
        }
    }];
}

RCT_EXPORT_METHOD(downloadPatchFromPpk:(NSDictionary *)options
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
    [self doPushy:PushyTypePatchFromPpk options:options callback:^(NSError *error) {
        if (error) {
            reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error);
        }
        else {
            resolve(nil);
        }
    }];
}

RCT_EXPORT_METHOD(setNeedUpdate:(NSDictionary *)options
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
    NSString *hash = options[@"hash"];
    
    if (hash.length) {
        NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
        NSString *lastVersion = nil;
        if ([defaults objectForKey:keyPushyInfo]) {
            NSDictionary *pushyInfo = [defaults objectForKey:keyPushyInfo];
            lastVersion = pushyInfo[paramCurrentVersion];
        }
        
        NSMutableDictionary *newInfo = [[NSMutableDictionary alloc] init];
        newInfo[paramCurrentVersion] = hash;
        newInfo[paramLastVersion] = lastVersion;
        newInfo[paramIsFirstTime] = @(YES);
        newInfo[paramIsFirstLoadOk] = @(NO);
        [defaults setObject:newInfo forKey:keyPushyInfo];
        
        
        resolve(@true);
    } else {
        reject(@"执行报错", nil, nil);
    }
}

RCT_EXPORT_METHOD(reloadUpdate:(NSDictionary *)options
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
    @try {
        NSString *hash = options[@"hash"];
        if (hash.length) {
            // 只在 setNeedUpdate 成功后 resolve
            [self setNeedUpdate:options resolver:^(id result) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    #if __has_include("RCTReloadCommand.h")
                        // reload 0.62+
                        RCTReloadCommandSetBundleURL([[self class] bundleURL]);
                        RCTTriggerReloadCommandListeners(@"pushy reloadUpdate");
                    #else
                        [self.bridge reload];
                    #endif
                });
                resolve(@true);
            } rejecter:^(NSString *code, NSString *message, NSError *error) {
                reject(code, message, error);
            }];
        } else {
            reject(@"执行报错", nil, nil);
        }
    }
    @catch (NSException *exception) {
        reject(@"执行报错", nil, nil);
    }
}

RCT_EXPORT_METHOD(restartApp:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
    @try {
        dispatch_async(dispatch_get_main_queue(), ^{
            #if __has_include("RCTReloadCommand.h")
                // reload 0.62+
                RCTReloadCommandSetBundleURL([[self class] bundleURL]);
                RCTTriggerReloadCommandListeners(@"pushy restartApp");
            #else
                [self.bridge reload];
            #endif
        });

        resolve(@true);
    }
    @catch (NSException *exception) {
        reject(@"执行报错", exception.reason, nil);
    }
}

RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
                                    rejecter:(RCTPromiseRejectBlock)reject)
{
    #if DEBUG
    resolve(@true);
    #else
    
    @try {
        // up package info
        NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
        NSMutableDictionary *pushyInfo = [[NSMutableDictionary alloc] initWithDictionary:[defaults objectForKey:keyPushyInfo]];
        [pushyInfo setObject:@(NO) forKey:paramIsFirstTime];
        [pushyInfo setObject:@(YES) forKey:paramIsFirstLoadOk];
        
        NSString *lastVersion = pushyInfo[paramLastVersion];
        NSString *curVersion = pushyInfo[paramCurrentVersion];
        if (lastVersion != nil && ![lastVersion isEqualToString:curVersion]) {
            [pushyInfo removeObjectForKey:[keyHashInfo stringByAppendingString:lastVersion]];
        }
        [defaults setObject:pushyInfo forKey:keyPushyInfo];
        
        
        // clear other package dir
        [self clearInvalidFiles];
        resolve(@true);
    }
    @catch (NSException *exception) {
        reject(@"执行报错", nil, nil);
    }
    #endif
}



#pragma mark - private
- (NSArray<NSString *> *)supportedEvents
{
  return @[
      EVENT_PROGRESS_DOWNLOAD, 
    //   EVENT_PROGRESS_UNZIP
      ];
}

// Will be called when this module's first listener is added.
-(void)startObserving {
    hasListeners = YES;
    // Set up any upstream listeners or background tasks as necessary
}

// Will be called when this module's last listener is removed, or on dealloc.
-(void)stopObserving {
    hasListeners = NO;
    // Remove upstream listeners, stop unnecessary background tasks
}

- (BOOL) isBlankString:(NSString *)string {
    if (string == nil || string == NULL) {
        return YES;
    }
    if ([string isKindOfClass:[NSNull class]]) {
        return YES;
    }
    if ([[string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] length]==0) {
        return YES;
    }
    return NO;
}


- (void)doPushy:(PushyType)type options:(NSDictionary *)options callback:(void (^)(NSError *error))callback
{
    NSString *updateUrl = [RCTConvert NSString:options[@"updateUrl"]];
    NSString *hash = [RCTConvert NSString:options[@"hash"]];

    if (updateUrl.length <= 0 || hash.length <= 0) {
        callback([self errorWithMessage:ERROR_OPTIONS]);
        return;
    }
    NSString *originHash = [RCTConvert NSString:options[@"originHash"]];
    if (type == PushyTypePatchFromPpk && [self isBlankString:originHash]) {
        callback([self errorWithMessage:ERROR_OPTIONS]);
        return;
    }
    
    NSString *dir = [RCTPushy downloadDir];
    BOOL success = [_fileManager createDir:dir];
    if (!success) {
        callback([self errorWithMessage:ERROR_FILE_OPERATION]);
        return;
    }

    NSString *zipFilePath = [dir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@%@",hash, [self zipExtension:type]]];
//    NSString *unzipDir = [dir stringByAppendingPathComponent:hash];

    RCTLogInfo(@"RCTPushy -- download file %@", updateUrl);
    [RCTPushyDownloader download:updateUrl savePath:zipFilePath progressHandler:^(long long receivedBytes, long long totalBytes) {
        if (self->hasListeners) {
            [self sendEventWithName:EVENT_PROGRESS_DOWNLOAD body:@{
                PARAM_PROGRESS_HASH:hash,
                PARAM_PROGRESS_RECEIVED:[NSNumber numberWithLongLong:receivedBytes],
                PARAM_PROGRESS_TOTAL:[NSNumber numberWithLongLong:totalBytes]
            }];
        }
    } completionHandler:^(NSString *path, NSError *error) {
        if (error) {
            callback(error);
        }
        else {
            RCTLogInfo(@"RCTPushy -- unzip file %@", zipFilePath);
            NSString *unzipFilePath = [dir stringByAppendingPathComponent:hash];
            [self->_fileManager unzipFileAtPath:zipFilePath toDestination:unzipFilePath progressHandler:^(NSString *entry,long entryNumber, long total) {
                // if (self->hasListeners) {
                //     [self sendEventWithName:EVENT_PROGRESS_UNZIP
                //                        body:@{
                //                            PARAM_PROGRESS_HASH:hash,
                //                            PARAM_PROGRESS_RECEIVED:[NSNumber numberWithLong:entryNumber],
                //                            PARAM_PROGRESS_TOTAL:[NSNumber numberWithLong:total]
                //                        }];
                // }
                
            } completionHandler:^(NSString *path, BOOL succeeded, NSError *error) {
                dispatch_async(self->_methodQueue, ^{
                    if (error) {
                        callback(error);
                    }
                    else {
                        switch (type) {
                            case PushyTypePatchFromPackage:
                            {
                                NSString *sourceOrigin = [[NSBundle mainBundle] resourcePath];
                                NSString *bundleOrigin = [[RCTPushy binaryBundleURL] path];
                                [self patch:hash fromBundle:bundleOrigin source:sourceOrigin callback:callback];
                            }
                                break;
                            case PushyTypePatchFromPpk:
                            {
                                NSString *lastVersionDir = [dir stringByAppendingPathComponent:originHash];
                                
                                NSString *sourceOrigin = lastVersionDir;
                                NSString *bundleOrigin = [lastVersionDir stringByAppendingPathComponent:BUNDLE_FILE_NAME];
                                [self patch:hash fromBundle:bundleOrigin source:sourceOrigin callback:callback];
                            }
                                break;
                            default:
                                callback(nil);
                                break;
                        }
                    }
                });
            }];
        }
    }];
}

- (void)_dopatch:(NSString *)hash fromBundle:(NSString *)bundleOrigin source:(NSString *)sourceOrigin
        callback:(void (^)(NSError *error))callback
{
    NSString *unzipDir = [[RCTPushy downloadDir] stringByAppendingPathComponent:hash];
    NSString *sourcePatch = [unzipDir stringByAppendingPathComponent:SOURCE_PATCH_NAME];
    NSString *bundlePatch = [unzipDir stringByAppendingPathComponent:BUNDLE_PATCH_NAME];
    
    NSString *destination = [unzipDir stringByAppendingPathComponent:BUNDLE_FILE_NAME];
    void (^completionHandler)(BOOL success) = ^(BOOL success) {
        if (success) {
            NSData *data = [NSData dataWithContentsOfFile:sourcePatch];
            NSError *error = nil;
            NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
            if (error) {
                callback(error);
                return;
            }
            
            NSDictionary *copies = json[@"copies"];
            NSDictionary *deletes = json[@"deletes"];

            [self->_fileManager copyFiles:copies fromDir:sourceOrigin toDir:unzipDir deletes:deletes completionHandler:^(NSError *error) {
                if (error) {
                    callback(error);
                }
                else {
                    callback(nil);
                }
            }];
        }
        else {
            callback([self errorWithMessage:ERROR_HDIFFPATCH]);
        }
    };
    
    @try {
        [_fileManager hdiffFileAtPath:bundlePatch fromOrigin:bundleOrigin toDestination:destination completionHandler:completionHandler];
    }
    @catch (NSException *exception) {
        NSLog(@"Pushy _dopatch error: exception occurred during hdiffFileAtPath: %@, reason: %@", 
              exception.name, exception.reason);
        callback([self errorWithMessage:ERROR_HDIFFPATCH]);
    }
}

- (void)patch:(NSString *)hash fromBundle:(NSString *)bundleOrigin source:(NSString *)sourceOrigin callback:(void (^)(NSError *error))callback
{
    [self _dopatch:hash fromBundle:bundleOrigin source:sourceOrigin callback:callback];
}

- (void)clearInvalidFiles
{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSDictionary *pushyInfo = [defaults objectForKey:keyPushyInfo];
    NSString *curVersion = [pushyInfo objectForKey:paramCurrentVersion];
    
    NSString *downloadDir = [RCTPushy downloadDir];
    NSError *error = nil;
    NSArray *list = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:downloadDir error:&error];
    if (error) {
        return;
    }
    
    for(NSString *fileName in list) {
        if (![fileName isEqualToString:curVersion]) {
            NSString *filePath = [downloadDir stringByAppendingPathComponent:fileName];
            NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error];
            if (error) {
                continue;
            }
            NSDate *modificationDate = [attributes fileModificationDate];
            if ([[NSDate date] timeIntervalSinceDate:modificationDate] > 7 * 24 * 60 * 60) {
                [_fileManager removeFile:filePath completionHandler:nil];
            }
        }
    }
}

- (NSString *)zipExtension:(PushyType)type
{
    switch (type) {
        case PushyTypeFullDownload:
            return @".ppk";
        case PushyTypePatchFromPackage:
            return @".ipa.patch";
        case PushyTypePatchFromPpk:
            return @".ppk.patch";
        default:
            break;
    }
}

- (NSError *)errorWithMessage:(NSString *)errorMessage
{
    return [NSError errorWithDomain:@"cn.reactnative.pushy"
                               code:-1
                           userInfo:@{ NSLocalizedDescriptionKey: errorMessage}];
}

+ (NSString *)downloadDir
{
    NSString *directory = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) firstObject];
    NSString *downloadDir = [directory stringByAppendingPathComponent:@"rctpushy"];
    
    return downloadDir;
}

+ (NSURL *)binaryBundleURL
{
    NSURL *url = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
    return url;
}

+ (NSString *)packageVersion
{
    static NSString *version = nil;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
        version = [infoDictionary objectForKey:@"CFBundleShortVersionString"];
    });
    return version;
}

+ (NSString *)buildTime
{
#if DEBUG
    return @"0";
#else
    static NSString *buildTime;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
      NSString *buildTimePath = [[NSBundle mainBundle] pathForResource:@"pushy_build_time" ofType:@"txt"];
      buildTime = [[NSString stringWithContentsOfFile:buildTimePath encoding:NSUTF8StringEncoding error:nil]
                 stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
    });
    return buildTime;
#endif
}

// Thanks to this guard, we won't compile this code when we build for the old architecture.
#ifdef RCT_NEW_ARCH_ENABLED
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
    (const facebook::react::ObjCTurboModule::InitParams &)params
{
    return std::make_shared<facebook::react::NativePushySpecJSI>(params);
}
#endif

@end