//
//  Pushwoosh.mm
//  Pushwoosh React Native Plugin
//  (c) Pushwoosh 2016
//

#import "Pushwoosh.h"

#import <React/RCTUtils.h>
#import <React/RCTBridge.h>
#import "PWEventDispatcher.h"
#import <React/RCTEventDispatcher.h>
#import <React/RCTConvert.h>
#import <React/RCTLinkingManager.h>

#import <UserNotifications/UserNotifications.h>
#import <PushwooshFramework/PushNotificationManager.h>
#import <PushwooshCore/PushwooshConfig.h>

#import <objc/runtime.h>
#import <objc/message.h>

#define kPushwooshPluginImplementationInfoPlistKey @"Pushwoosh_PLUGIN_NOTIFICATION_HANDLER"

#define PW_COMMUNICATION_ENABLED_KEY @"PushwooshCommunicationEnabled"
#define PW_COMMUNICATION_ENABLED_PLIST_KEY @"Pushwoosh_ALLOW_SERVER_COMMUNICATION"


static id objectOrNull(id object) {
    if (object) {
        return object;
    } else {
        return [NSNull null];
    }
}

typedef void (^PWEmailsCompletion)(NSError *error);

// The SDK invokes the completion once per address, an RCTResponseSenderBlock only once:
// the first error wins, success is reported after the last address.
static PWEmailsCompletion emailsCompletionHandler(NSUInteger addressCount, RCTResponseSenderBlock successCallback, RCTResponseSenderBlock errorCallback) {
    NSObject *lock = [NSObject new];
    __block NSUInteger completedCount = 0;
    __block BOOL responded = NO;

    return ^(NSError *error) {
        @synchronized (lock) {
            if (responded) {
                return;
            }

            if (error) {
                responded = YES;
                if (errorCallback) {
                    errorCallback(@[ objectOrNull([error localizedDescription]) ]);
                }
                return;
            }

            completedCount++;
            if (completedCount >= addressCount) {
                responded = YES;
                if (successCallback) {
                    successCallback(@[]);
                }
            }
        }
    };
}

static NSDictionary * gStartPushData = nil;
static BOOL gStartPushReplayed = NO;  // Launch push already replayed into the JS events by init:
static NSURL * gPushDeepLinkURL = nil;  // Deep link URL for New Architecture support
static NSString * gEarlyHandledHash = nil;  // Last push hash the cold-start delegate accepted, to avoid double handling
static NSString * gPendingWebLaunchLink = nil;  // http(s) link of the launch push, until the SDK hands it to RCTLinkingManager
static NSUInteger gPendingWebLaunchLinkGeneration = 0;  // Bumped on every arm, so a stale delivery timeout leaves a newer link alone
static NSString * gLastArmedWebLaunchLink = nil;  // Survives delivery, so re-arming the same push is recognised
static NSTimeInterval gWebLaunchLinkDeadline = 0;  // System uptime at which the window gLastArmedWebLaunchLink first opened ends
static BOOL gInitialURLAnswered = NO;  // JS called getInitialURL() since gLastArmedWebLaunchLink was first armed
static BOOL gPluginInitialized = NO;  // init: ran, so JS is up and a tapped push is no longer a launch push
static NSMutableArray<void (^)(NSString *)> * gInitialURLWaiters = nil;  // getInitialURL() calls held for the pending link
// The SDK waits 0.2 s after accepting the push, then fetches apple-app-site-association with a 5 s timeout.
static NSTimeInterval const kPWWebLaunchLinkDeliveryTimeout = 6.0;
static NSString * const kRegistrationSuccesEvent = @"PWRegistrationSuccess";
static NSString * const kRegistrationErrorEvent = @"PWRegistrationError";
static NSString * const kPushReceivedEvent = @"PWPushReceived";
static NSString * const kPushOpenEvent = @"PWPushOpen";

static NSString * const kPushOpenJSEvent = @"pushOpened";
static NSString * const kPushReceivedJSEvent = @"pushReceived";

/// Returns the push link only when it is a deep link this app can route itself, otherwise nil.
///
/// http/https links are left to the native SDK: it checks the host's apple-app-site-association
/// and either hands the link to the app's Universal Links handler (own domain) or opens the
/// browser (anyone else's). Handing those to RCTLinking too made every external link land in JS
/// as if it were an in-app destination.
static NSURL * pwplugin_deepLinkURLFromPushLink(id link) {
    if (![link isKindOfClass:[NSString class]] || [link length] == 0) {
        return nil;
    }

    NSURL *url = [NSURL URLWithString:link];
    NSString *scheme = url.scheme.lowercaseString;

    if (scheme.length == 0 || [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"]) {
        return nil;
    }

    return url;
}

/// Returns the http(s) push link as `URL.absoluteString`, the form RCTLinkingManager posts, otherwise nil.
static NSString * pwplugin_webLinkFromPushLink(id link) {
    if (![link isKindOfClass:[NSString class]] || [link length] == 0) {
        return nil;
    }

    NSURL *url = [NSURL URLWithString:link];
    NSString *scheme = url.scheme.lowercaseString;
    if ([scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"]) {
        return url.absoluteString;
    }

    return nil;
}

/// Answers every getInitialURL() call held for the pending link: with `url` when it arrived, otherwise
/// with what RCTLinkingManager itself reports. JS has read its initial URL either way.
static void pwplugin_settleInitialURLWaiters(NSString *url) {
    NSArray<void (^)(NSString *)> *waiters = gInitialURLWaiters;
    gInitialURLWaiters = nil;
    gInitialURLAnswered = YES;
    for (void (^waiter)(NSString *) in waiters) {
        waiter(url);
    }
}

/// Arms the launch push's web link for the SDK's delivery window; called by every launch-push site.
/// A push without a web link disarms it, so a later Safari open of the same URL is not caught.
static void pwplugin_rememberWebLaunchLink(NSDictionary *push) {
    NSString *link = pwplugin_webLinkFromPushLink(push[@"l"]);
    NSTimeInterval now = [NSProcessInfo processInfo].systemUptime;

    // The SDK accepts the same launch push at init() again, possibly after JS read getInitialURL().
    // The window stays the one the tap opened: neither extended nor opened again once it ended.
    if (link && [link isEqualToString:gLastArmedWebLaunchLink]) {
        if (now < gWebLaunchLinkDeadline) {
            gPendingWebLaunchLink = link;
        }
        return;
    }

    gInitialURLAnswered = NO;
    gLastArmedWebLaunchLink = link;
    gPendingWebLaunchLink = link;
    gWebLaunchLinkDeadline = now + kPWWebLaunchLinkDeliveryTimeout;
    NSUInteger generation = ++gPendingWebLaunchLinkGeneration;

    if (!link) {
        if (gInitialURLWaiters) {
            pwplugin_settleInitialURLWaiters(nil);
        }
        return;
    }

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kPWWebLaunchLinkDeliveryTimeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        if (generation == gPendingWebLaunchLinkGeneration) {
            gPendingWebLaunchLink = nil;
            if (gInitialURLWaiters) {
                pwplugin_settleInitialURLWaiters(nil);
            }
        }
    });
}

/// Catches the launch push's Universal Link on its way to RCTLinkingManager: resolves held getInitialURL()
/// calls, or parks it for the first one; a link after getInitialURL() answered is left to the `url` event.
static void pwplugin_handleOpenURLNotification(NSNotification *notification) {
    id url = notification.userInfo[@"url"];
    if (!gPendingWebLaunchLink || ![url isKindOfClass:[NSString class]] || ![url isEqualToString:gPendingWebLaunchLink]) {
        return;
    }

    gPendingWebLaunchLink = nil;
    if (gInitialURLWaiters) {
        pwplugin_settleInitialURLWaiters(url);
    } else if (!gInitialURLAnswered) {
        gPushDeepLinkURL = [NSURL URLWithString:url];
    }
}

// Bridgeless hands a module an RCTBridgeProxy: an NSProxy that carries none of NSObject's KVC and
// turns an unknown selector into a logged no-op. object_getClass asks without messaging it.
static BOOL pwplugin_isRealBridge(id bridge) {
    if (bridge == nil) {
        return NO;
    }

    Class bridgeProxyClass = NSClassFromString(@"RCTBridgeProxy");
    return bridgeProxyClass == nil || object_getClass(bridge) != bridgeProxyClass;
}

@interface PushwooshPlugin (InnerPushwooshPlugin)

- (void) application:(UIApplication *)application pwplugin_didRegisterWithDeviceToken:(NSData *)deviceToken;
- (void) application:(UIApplication *)application pwplugin_didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler;
- (void) application:(UIApplication *)application pwplugin_didFailToRegisterForRemoteNotificationsWithError:(NSError *)error;

@end

@interface RCTLinkingManager (PushwooshSwizzle)
- (void)pwplugin_original_getInitialURL:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;
@end

static void pwplugin_originalGetInitialURL(id self, RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject) {
    SEL originalSelector = @selector(pwplugin_original_getInitialURL:reject:);
    if ([self respondsToSelector:originalSelector]) {
        void (*originalIMP)(id, SEL, RCTPromiseResolveBlock, RCTPromiseRejectBlock);
        originalIMP = (void (*)(id, SEL, RCTPromiseResolveBlock, RCTPromiseRejectBlock))objc_msgSend;
        originalIMP(self, originalSelector, resolve, reject);
    } else {
        resolve([NSNull null]);
    }
}

static char kPWInitialURLLaunchLinkKey;

/// Answers getInitialURL() with the launch push's link and keeps it on the RCTLinkingManager that asked.
/// React Native creates a new one for every JS runtime, so a JS reload or an OTA update starts without it.
static void pwplugin_resolveWithLaunchLink(id linkingManager, NSString *url, RCTPromiseResolveBlock resolve) {
    objc_setAssociatedObject(linkingManager, &kPWInitialURLLaunchLinkKey, url, OBJC_ASSOCIATION_COPY_NONATOMIC);
    resolve(url);
}

// Swizzled getInitialURL for RCTLinkingManager (New Architecture support)
static void pwplugin_getInitialURL(id self, SEL _cmd, RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject) {
    // If we have a saved deep link URL from push notification, return it
    if (gPushDeepLinkURL) {
        NSURL *url = gPushDeepLinkURL;
        gPushDeepLinkURL = nil;  // Spent: a reloaded JS runtime must not get it again
        gInitialURLAnswered = YES;
        pwplugin_resolveWithLaunchLink(self, url.absoluteString, resolve);
        return;
    }

    // Held until the link arrives or its delivery window ends: the SDK may still be checking the
    // host's apple-app-site-association, which takes seconds on a network round trip.
    if (gPendingWebLaunchLink && !gInitialURLAnswered) {
        if (!gInitialURLWaiters) {
            gInitialURLWaiters = [NSMutableArray array];
        }
        [gInitialURLWaiters addObject:[^(NSString *url) {
            if (url) {
                pwplugin_resolveWithLaunchLink(self, url, resolve);
            } else {
                pwplugin_originalGetInitialURL(self, resolve, reject);
            }
        } copy]];
        return;
    }

    gInitialURLAnswered = YES;

    // Every later call of this JS runtime gets the link the first one got, as with React Native's own
    // launch URL: an app may read it in several places, an analytics SDK first and its router later.
    NSString *launchLink = objc_getAssociatedObject(self, &kPWInitialURLLaunchLinkKey);
    if (launchLink) {
        resolve(launchLink);
        return;
    }

    pwplugin_originalGetInitialURL(self, resolve, reject);
}

void pushwoosh_swizzle(Class cls, SEL fromChange, SEL toChange, IMP impl, const char * signature) {
    Method method = nil;
    method = class_getInstanceMethod(cls, fromChange);
    
    if (method) {
        //method exists add a new method and swap with original
        class_addMethod(cls, toChange, impl, signature);
        method_exchangeImplementations(class_getInstanceMethod(cls, fromChange), class_getInstanceMethod(cls, toChange));
    } else {
        //just add as orignal method
        class_addMethod(cls, fromChange, impl, signature);
    }
}

@implementation PushwooshPlugin {
    NSString *lastPushwooshHash;
    
    API_AVAILABLE(ios(10))
    __weak id<UNUserNotificationCenterDelegate> _originalNotificationCenterDelegate;
    API_AVAILABLE(ios(10))
    struct {
        unsigned int willPresentNotification : 1;
        unsigned int didReceiveNotificationResponse : 1;
        unsigned int openSettingsForNotification : 1;
    } _originalNotificationCenterDelegateResponds;
}

#pragma mark - Pushwoosh RCTBridgeModule

// No custom name: React Native looks a TurboModule up by NSClassFromString(name) before it
// consults the RCT_EXPORT_MODULE registry, and "Pushwoosh" is the native SDK's own class.
RCT_EXPORT_MODULE();

#ifdef RCT_NEW_ARCH_ENABLED
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
    return std::make_shared<facebook::react::NativePushwooshSpecJSI>(params);
}
#endif

- (dispatch_queue_t)methodQueue {
    return dispatch_get_main_queue();
}

// Overriding init makes React Native infer main queue setup on its own; saying so keeps that
// contract explicit, since the module claims the notification center delegate right away.
+ (BOOL)requiresMainQueueSetup {
    return YES;
}

+ (NSString *)getPluginImplementationInfoPlistKey {
    return [[NSBundle mainBundle] objectForInfoDictionaryKey:kPushwooshPluginImplementationInfoPlistKey];
}

RCT_EXPORT_METHOD(init:(NSDictionary*)config success:(RCTResponseSenderBlock)success error:(RCTResponseSenderBlock)error) {
    NSString *appCode = config[@"pw_appid"];
    NSString *notificationHandling = config[@"pw_notification_handling"];
    
    if (!appCode || ![appCode isKindOfClass:[NSString class]]) {
        if (error) {
            error(@[ @"pw_appid is missing" ]);
        }
        
        return;
    }
    
    gPluginInitialized = YES;
    [PushNotificationManager initializeWithAppCode:appCode appName:nil];
    [[PushNotificationManager pushManager] sendAppOpen];
    [PushNotificationManager pushManager].delegate = self;

    [PushwooshPlugin swizzleNotificationSettingsHandler];

    // We set Pushwoosh UNUserNotificationCenter delegate unless CUSTOM is specified in the config
    if(![notificationHandling isEqualToString:@"CUSTOM"]) {
        if (![PushwooshPlugin getPluginImplementationInfoPlistKey]) {
            [UNUserNotificationCenter currentNotificationCenter].delegate = [PushNotificationManager pushManager].notificationCenterDelegate;
        } else {
            if (@available(iOS 10, *)) {
                BOOL shouldReplaceDelegate = YES;
                
                UNUserNotificationCenter *notificationCenter = [UNUserNotificationCenter currentNotificationCenter];
                
#if !TARGET_OS_OSX
                if ([notificationCenter.delegate conformsToProtocol:@protocol(PushNotificationDelegate)]) {
                    shouldReplaceDelegate = NO;
                }
#endif
                
                if (notificationCenter.delegate != nil && shouldReplaceDelegate) {
                    _originalNotificationCenterDelegate = notificationCenter.delegate;
                    _originalNotificationCenterDelegateResponds.openSettingsForNotification =
                    (unsigned int)[_originalNotificationCenterDelegate
                                   respondsToSelector:@selector(userNotificationCenter:openSettingsForNotification:)];
                    _originalNotificationCenterDelegateResponds.willPresentNotification =
                    (unsigned int)[_originalNotificationCenterDelegate
                                   respondsToSelector:@selector(userNotificationCenter:
                                                                willPresentNotification:withCompletionHandler:)];
                    _originalNotificationCenterDelegateResponds.didReceiveNotificationResponse =
                    (unsigned int)[_originalNotificationCenterDelegate
                                   respondsToSelector:@selector(userNotificationCenter:
                                                                didReceiveNotificationResponse:withCompletionHandler:)];
                }
                
                if (shouldReplaceDelegate) {
                    __strong PushwooshPlugin<UNUserNotificationCenterDelegate> *strongSelf = (PushwooshPlugin<UNUserNotificationCenterDelegate> *)self;
                    notificationCenter.delegate = (id<UNUserNotificationCenterDelegate>)strongSelf;
                }
            }
        }
    }

    if (success) {
        success(@[]);
    }
    
    // Once per launch push. Neither source clears itself here - gStartPushData is consumed by the
    // legacy onPushOpen:, launchNotification belongs to the SDK - so a second init() (a consent
    // flow initialising after the user agreed, an app switching application code, a Fast Refresh
    // reload) used to replay the same push as pushReceived and pushOpened again and to re-arm a
    // deep link that Linking.getInitialURL() had already consumed, sending the user back to the
    // screen the push opened the first time.
    if (!gStartPushReplayed) {
        gStartPushReplayed = YES;

        if (gStartPushData) {
            NSURL *deepLink = pwplugin_deepLinkURLFromPushLink(gStartPushData[@"l"]);

            if (deepLink) {
                if (!gPushDeepLinkURL) {
                    gPushDeepLinkURL = deepLink;
                }

                // Writing the link into launchOptions is what makes RCTLinking report it, but only
                // a real bridge accepts the write. Bridgeless reads it from the swizzled
                // getInitialURL.
                if (pwplugin_isRealBridge(self.bridge)) {
                    NSMutableDictionary *launchOptions = self.bridge.launchOptions.mutableCopy;
                    launchOptions[UIApplicationLaunchOptionsURLKey] = deepLink;
                    [self.bridge setValue:launchOptions forKey:@"launchOptions"];
                }
            }

            [self sendJSEvent:kPushReceivedJSEvent withArgs:gStartPushData];
            [self sendJSEvent:kPushOpenJSEvent withArgs:gStartPushData];
        } else if([PushNotificationManager pushManager].launchNotification) {
            [self sendJSEvent:kPushReceivedJSEvent withArgs:[PushNotificationManager pushManager].launchNotification];
            [self sendJSEvent:kPushOpenJSEvent withArgs:[PushNotificationManager pushManager].launchNotification];
        }
    }
}

RCT_EXPORT_METHOD(registerForPushNotifications:(RCTResponseSenderBlock)success error:(RCTResponseSenderBlock)error) {
    [[PWEventDispatcher sharedDispatcher] subscribe:success toEvent:kRegistrationSuccesEvent];
    [[PWEventDispatcher sharedDispatcher] subscribe:error toEvent:kRegistrationErrorEvent];
    
    [[PushNotificationManager pushManager] registerForPushNotifications];
}

RCT_EXPORT_METHOD(unregister:(RCTResponseSenderBlock)successCallback error:(RCTResponseSenderBlock)errorCallback) {
    [[PushNotificationManager pushManager] unregisterForPushNotificationsWithCompletion:^(NSError *error) {
        if (!error && successCallback) {
            successCallback(@[]);
        }
        
        if (error && errorCallback) {
            errorCallback(@[ objectOrNull([error localizedDescription]) ]);
        }
    }];
}

RCT_EXPORT_METHOD(onPushOpen:(RCTResponseSenderBlock)callback) {
    [[PWEventDispatcher sharedDispatcher] subscribe:callback toEvent:kPushOpenEvent];
    
    if (gStartPushData) {
        NSDictionary *pushData = gStartPushData;
        gStartPushData = nil;
        [[PWEventDispatcher sharedDispatcher] dispatchEvent:kPushOpenEvent withArgs:@[ objectOrNull(pushData) ]];
    }
}

RCT_EXPORT_METHOD(onPushReceived:(RCTResponseSenderBlock)callback) {
    [[PWEventDispatcher sharedDispatcher] subscribe:callback toEvent:kPushReceivedEvent];
    
    if (gStartPushData) {
        NSDictionary *pushData = gStartPushData;
        gStartPushData = nil;
        [[PWEventDispatcher sharedDispatcher] dispatchEvent:kPushReceivedEvent withArgs:@[ objectOrNull(pushData) ]];
    }
}

RCT_EXPORT_METHOD(getHwid:(RCTResponseSenderBlock)callback) {
    if (callback) {
        callback(@[ [[PushNotificationManager pushManager] getHWID] ]);
    }
}

RCT_EXPORT_METHOD(getUserId:(RCTResponseSenderBlock)callback) {
    if (callback) {
        callback(@[ [[Pushwoosh sharedInstance] getUserId] ]);
    }
}

RCT_EXPORT_METHOD(getPushToken:(RCTResponseSenderBlock)callback) {
    if (callback) {
        callback(@[ objectOrNull([[PushNotificationManager pushManager] getPushToken]) ]);
    }
}

RCT_EXPORT_METHOD(setEmails:(NSArray *)emails success:(RCTResponseSenderBlock)successCallback error:(RCTResponseSenderBlock)errorCallback) {
    if (emails.count == 0) {
        // The SDK ignores an empty list without invoking the completion block.
        if (errorCallback) {
            errorCallback(@[ @"emails must be a non-empty array" ]);
        }
        return;
    }

    [[Pushwoosh sharedInstance] setEmails:emails completion:emailsCompletionHandler(emails.count, successCallback, errorCallback)];
}

RCT_EXPORT_METHOD(setUserEmails:(NSString*)userId emails:(NSArray *)emails success:(RCTResponseSenderBlock)successCallback error:(RCTResponseSenderBlock)errorCallback) {
    if (userId.length == 0) {
        // The SDK ignores an empty user id without invoking the completion block.
        if (errorCallback) {
            errorCallback(@[ @"userId must not be empty" ]);
        }
        return;
    }

    if (emails.count == 0) {
        if (errorCallback) {
            errorCallback(@[ @"emails must be a non-empty array" ]);
        }
        return;
    }

    [[Pushwoosh sharedInstance] setUser:userId emails:emails completion:emailsCompletionHandler(emails.count, successCallback, errorCallback)];
}

RCT_EXPORT_METHOD(setTags:(NSDictionary*)tags success:(RCTResponseSenderBlock)successCallback error:(RCTResponseSenderBlock)errorCallback) {
    [[PushNotificationManager pushManager] setTags:tags withCompletion:^(NSError* error) {
        if (!error && successCallback) {
            successCallback(@[]);
        }
        
        if (error && errorCallback) {
            errorCallback(@[ objectOrNull([error localizedDescription]) ]);
        }
    }];
}

RCT_EXPORT_METHOD(getTags:(RCTResponseSenderBlock)successCallback error:(RCTResponseSenderBlock)errorCallback) {
    [[PushNotificationManager pushManager] loadTags:^(NSDictionary* tags) {
        if (successCallback) {
            successCallback(@[ tags ]);
        }
    } error:^(NSError *error) {
        if (errorCallback) {
            errorCallback(@[ objectOrNull([error localizedDescription]) ]);
        }
    }];
}

RCT_EXPORT_METHOD(setShowPushnotificationAlert:(BOOL)showPushnotificationAlert) {
    [[PushNotificationManager pushManager] setShowPushnotificationAlert:showPushnotificationAlert];
}

RCT_EXPORT_METHOD(getShowPushnotificationAlert:(RCTResponseSenderBlock)callback) {
    if(callback) {
        callback(@[ @([PushNotificationManager pushManager].showPushnotificationAlert) ]);
    }
}

RCT_EXPORT_METHOD(setUserId:(NSString*)userId success:(RCTResponseSenderBlock)successCallback error:(RCTResponseSenderBlock)errorCallback) {
    
    [[Pushwoosh sharedInstance] setUserId:userId completion:^(NSError *error) {
        if (!error && successCallback) {
            successCallback(@[]);
        }
        
        if (error && errorCallback) {
            errorCallback(@[ objectOrNull([error localizedDescription]) ]);
        }
    }];
}

RCT_EXPORT_METHOD(postEvent:(NSString*)event attributes:(NSDictionary*)attributes) {
    [[PWInAppManager sharedManager] postEvent:event withAttributes:attributes];
}

RCT_EXPORT_METHOD(setApplicationIconBadgeNumber:(double)badgeNumber) {
    dispatch_async(dispatch_get_main_queue(), ^{
        [UIApplication sharedApplication].applicationIconBadgeNumber = (NSInteger)badgeNumber;
    });
}

RCT_EXPORT_METHOD(setLanguage:(NSString *)language) {
    [PushNotificationManager pushManager].language = language;
}

RCT_EXPORT_METHOD(getApplicationIconBadgeNumber:(RCTResponseSenderBlock)callback) {
    if(callback) {
        dispatch_async(dispatch_get_main_queue(), ^{
           callback(@[ @([UIApplication sharedApplication].applicationIconBadgeNumber) ]);
        });
    }
}

RCT_EXPORT_METHOD(addToApplicationIconBadgeNumber:(double)badgeNumber) {
    dispatch_async(dispatch_get_main_queue(), ^{
        [UIApplication sharedApplication].applicationIconBadgeNumber += (NSInteger)badgeNumber;
    });
}
    
RCT_EXPORT_METHOD(presentInboxUI:(NSDictionary *)styleDictionary) {
    NSString *resourceBundlePath = [[NSBundle mainBundle] pathForResource:@"PushwooshInboxBundle" ofType:@"bundle"];
    if (![NSBundle bundleWithPath:resourceBundlePath]) {
        NSLog(@"[Pushwoosh][presentInboxUI] Error: PushwooshInboxBundle.bundle not found. Please launch \"node node_modules/pushwoosh-react-native-plugin/scripts/add_inbox_ios_resources.js\" from project root directory or manually add node_modules/pushwoosh-react-native-plugin/src/ios/PushwooshInboxBundle.bundle to your project.");
    } else {
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            PWIInboxViewController *inboxViewController = [PWIInboxUI createInboxControllerWithStyle:[self inboxStyleForDictionary:styleDictionary]];
            inboxViewController.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Close", @"Close") style:UIBarButtonItemStylePlain target:self action:@selector(closeInbox)];
            [[PushwooshPlugin findRootViewController] presentViewController:[[UINavigationController alloc] initWithRootViewController:inboxViewController] animated:YES completion:nil];
            
            __weak __typeof__(self) wself = self;
            inboxViewController.onMessageClickBlock = ^(NSObject<PWInboxMessageProtocol> *message) {
                if (message.type == PWInboxMessageTypeDeeplink) {
                    [wself closeInbox];
                }
            };
        }];
    }
}

RCT_EXPORT_METHOD(messagesWithNoActionPerformedCount:(RCTResponseSenderBlock)callback) {
    [PWInbox messagesWithNoActionPerformedCountWithCompletion:^(NSInteger count, NSError *error) {
        if (callback) {
            callback(@[ @(count) ]);
        }
    }];
}

RCT_EXPORT_METHOD(unreadMessagesCount:(RCTResponseSenderBlock)callback) {
    [PWInbox unreadMessagesCountWithCompletion:^(NSInteger count, NSError *error) {
        if (callback) {
            callback(@[ @(count) ]);
        }
    }];
}

RCT_EXPORT_METHOD(messagesCount:(RCTResponseSenderBlock)callback) {
    [PWInbox messagesCountWithCompletion:^(NSInteger count, NSError *error) {
        if (callback) {
            callback(@[ @(count) ]);
        }
    }];
}

RCT_EXPORT_METHOD(loadMessages:(RCTResponseSenderBlock)success fail:(RCTResponseSenderBlock)fail) {
    [PWInbox loadMessagesWithCompletion:^(NSArray<NSObject<PWInboxMessageProtocol> *> *messages, NSError *error) {
        if (error) {
            if (fail) {
                fail(@[ objectOrNull([error localizedDescription]) ]);
            }
            return;
        }

        if (success) {
            NSMutableArray *array = [[NSMutableArray alloc] init];
            for (NSObject<PWInboxMessageProtocol> *message in messages) {
                [array addObject:[self inboxMessageToDictionary:message]];
            }
            success(@[ array ]);
        }
    }];
}

RCT_EXPORT_METHOD(readMessage:(NSString*)code) {
    NSArray* arr = [NSArray arrayWithObject:code];
    [PWInbox readMessagesWithCodes:arr];
}

RCT_EXPORT_METHOD(readMessages:(NSArray<NSString*>*)codes) {
    [PWInbox readMessagesWithCodes:codes];
}

RCT_EXPORT_METHOD(deleteMessage:(NSString*)code) {
    NSArray* arr = [NSArray arrayWithObject:code];
    [PWInbox deleteMessagesWithCodes:arr];
}

RCT_EXPORT_METHOD(deleteMessages:(NSArray<NSString*>*)codes) {
    [PWInbox deleteMessagesWithCodes:codes];
}

RCT_EXPORT_METHOD(performAction:(NSString*)code) {
    [PWInbox performActionForMessageWithCode:code];
}

#pragma mark -
#pragma mark - Swizzling

+ (void)swizzleNotificationSettingsHandler {
    if ([UIApplication sharedApplication].delegate == nil) {
        return;
    }
    
    if ([[[UIDevice currentDevice] systemVersion] floatValue] < 8.0) {
        return;
    }
    
    static Class appDelegateClass = nil;
    
    //do not swizzle the same class twice
    id delegate = [UIApplication sharedApplication].delegate;
    if(appDelegateClass == [delegate class]) {
        return;
    }
    
    appDelegateClass = [delegate class];
    
    pushwoosh_swizzle([delegate class], @selector(application:didRegisterForRemoteNotificationsWithDeviceToken:), @selector(application:pwplugin_didRegisterWithDeviceToken:), (IMP)pwplugin_didRegisterWithDeviceToken, "v@:::");
    pushwoosh_swizzle([delegate class], @selector(application:didFailToRegisterForRemoteNotificationsWithError:), @selector(application:pwplugin_didFailToRegisterForRemoteNotificationsWithError:), (IMP)pwplugin_didFailToRegisterForRemoteNotificationsWithError, "v@:::");
    pushwoosh_swizzle([delegate class], @selector(application:didReceiveRemoteNotification:fetchCompletionHandler:), @selector(application:pwplugin_didReceiveRemoteNotification:fetchCompletionHandler:), (IMP)pwplugin_didReceiveRemoteNotification, "v@::::");
}

void pwplugin_didReceiveRemoteNotification(id self, SEL _cmd, UIApplication * application, NSDictionary * userInfo, void (^completionHandler)(UIBackgroundFetchResult)) {
    if ([self respondsToSelector:@selector(application:pwplugin_didReceiveRemoteNotification:fetchCompletionHandler:)]) {
        [self application:application pwplugin_didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
    }
    
    [[Pushwoosh sharedInstance] handlePushReceived:userInfo];
}

void pwplugin_didRegisterWithDeviceToken(id self, SEL _cmd, id application, NSData *deviceToken) {
    if ([self respondsToSelector:@selector(application: pwplugin_didRegisterWithDeviceToken:)]) {
        [self application:application pwplugin_didRegisterWithDeviceToken:deviceToken];
    }
    
    [[Pushwoosh sharedInstance] handlePushRegistration:deviceToken];
}

void pwplugin_didFailToRegisterForRemoteNotificationsWithError(id self, SEL _cmd, UIApplication *application, NSError *error) {
    if ([self respondsToSelector:@selector(application:pwplugin_didFailToRegisterForRemoteNotificationsWithError:)]) {
        [self application:application pwplugin_didFailToRegisterForRemoteNotificationsWithError:error];
    }
    
    [[Pushwoosh sharedInstance] handlePushRegistrationFailure:error];
}

#pragma mark - UNUserNotificationCenter Delegate Methods
#pragma mark -

- (void)userNotificationCenter:(UNUserNotificationCenter *)center
       willPresentNotification:(UNNotification *)notification
         withCompletionHandler:
(void (^)(UNNotificationPresentationOptions options))completionHandler
API_AVAILABLE(ios(10.0)) {
    
    UNMutableNotificationContent *content = notification.request.content.mutableCopy;
    BOOL isPushwooshMessage = [PWMessage isPushwooshMessage:notification.request.content.userInfo];
    NSString *currentHash = content.userInfo[@"p"];
    BOOL isNeedToImplementInPlugin = [[PushwooshPlugin getPluginImplementationInfoPlistKey] boolValue];

    // 0 when this notification is not ours to present: someone else's, a repeat of the same
    // message, or a Pushwoosh push the app asked us not to show an alert for.
    UNNotificationPresentationOptions options = 0;

    if (isPushwooshMessage && isNeedToImplementInPlugin && ![lastPushwooshHash isEqualToString:currentHash]) {
        lastPushwooshHash = currentHash;

        if ([PushNotificationManager pushManager].showPushnotificationAlert) {
            options = UNNotificationPresentationOptionSound | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionAlert;
        } else {
            [[PushNotificationManager pushManager] handlePushReceived:content.userInfo];
        }
        
        [self sendJSEvent:kPushReceivedJSEvent withArgs:content.userInfo];
    }

    // UNUserNotificationCenter waits for exactly one answer: a second call is an error it logs,
    // and no call at all leaves the notification unpresented. Both used to be reachable here -
    // our own alert answered and the app's delegate answered again with the same handler, while a
    // notification that is neither ours nor claimed by an app delegate was never answered at all.
    // So whoever answers first decides and the rest is swallowed.
    __block BOOL answered = NO;
    void (^answerOnce)(UNNotificationPresentationOptions) = ^(UNNotificationPresentationOptions presentationOptions) {
        if (answered) {
            return;
        }
        answered = YES;
        completionHandler(presentationOptions);
    };

    // The SDK's alert goes up straight away, as before.
    if (options != 0) {
        answerOnce(options);
    }

    // The app's own delegate still gets the callback either way. A Pushwoosh message is handed
    // over with a delay, so the SDK presents its own alert first.
    if (_originalNotificationCenterDelegate != nil &&
        _originalNotificationCenterDelegateResponds.willPresentNotification) {
        
        dispatch_block_t presentationBlock = ^{
            [self->_originalNotificationCenterDelegate userNotificationCenter:center
                                                willPresentNotification:notification
                                                  withCompletionHandler:answerOnce];
        };
        
        if (isPushwooshMessage) {
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.59 * NSEC_PER_SEC)), dispatch_get_main_queue(), presentationBlock);
        } else {
            presentationBlock();
        }
        return;
    }

    answerOnce(options);
}

- (BOOL)isContentAvailablePush:(NSDictionary *)userInfo {
    NSDictionary *apsDict = userInfo[@"aps"];
    return apsDict[@"content-available"] != nil;
}

- (NSDictionary *)pushPayloadFromContent:(UNNotificationContent *)content {
    return [[content.userInfo objectForKey:@"pw_push"] isKindOfClass:[NSDictionary class]] ? [content.userInfo objectForKey:@"pw_push"] : content.userInfo;
}

- (BOOL)isRemoteNotification:(UNNotification *)notification {
    return [notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]];
}

- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
         withCompletionHandler:(void (^)(void))completionHandler
API_AVAILABLE(ios(10.0)) {
    BOOL isNeedToImplementInPlugin = [[PushwooshPlugin getPluginImplementationInfoPlistKey] boolValue];

    dispatch_block_t handlePushAcceptanceBlock = ^{
        if (![response.actionIdentifier isEqualToString:UNNotificationDismissActionIdentifier]) {
            if (![response.actionIdentifier isEqualToString:UNNotificationDefaultActionIdentifier] && [[PushNotificationManager pushManager].delegate respondsToSelector:@selector(onActionIdentifierReceived:withNotification:)]) {
                [[PushNotificationManager pushManager].delegate onActionIdentifierReceived:response.actionIdentifier withNotification:[self pushPayloadFromContent:response.notification.request.content]];
            }
        }
    };
    
    if ([self isRemoteNotification:response.notification] && [PWMessage isPushwooshMessage:response.notification.request.content.userInfo]) {
        NSDictionary *userInfo = response.notification.request.content.userInfo;

        if (isNeedToImplementInPlugin) {
            [[PushNotificationManager pushManager] handlePushAccepted:userInfo onStart:[self isOnStart:userInfo]];
        }

        handlePushAcceptanceBlock();
    } else if ([response.notification.request.content.userInfo objectForKey:@"pw_push"]) {
        handlePushAcceptanceBlock();
    }

    if (_originalNotificationCenterDelegate != nil &&
        _originalNotificationCenterDelegateResponds.didReceiveNotificationResponse) {
        [_originalNotificationCenterDelegate userNotificationCenter:center
                                     didReceiveNotificationResponse:response
                                              withCompletionHandler:completionHandler];
    } else {
        completionHandler();
    }
}

- (BOOL)isOnStart:(NSDictionary *)userInfo {
    NSDictionary *pushStartDictionary = [self startPushInfoFromInfoDictionary:userInfo];
    return pushStartDictionary != nil;
}

- (NSDictionary *)startPushInfoFromInfoDictionary:(NSDictionary *)userInfo {
    //try as launchOptions dictionary
    NSDictionary *pushDict = userInfo[UIApplicationLaunchOptionsRemoteNotificationKey];
    if (pushDict == nil) {
        id notification = userInfo[UIApplicationLaunchOptionsLocalNotificationKey];
        
        if (notification && [notification isKindOfClass:[UILocalNotification class]]) {
            pushDict = [notification userInfo];
            
            if (pushDict[@"pw_push"] == nil) {
                pushDict = nil;
            }
        }
    }

    return pushDict;
}

- (void)userNotificationCenter:(UNUserNotificationCenter *)center
   openSettingsForNotification:(nullable UNNotification *)notification
API_AVAILABLE(ios(10.0)) {
    if ([[PushNotificationManager pushManager].delegate respondsToSelector:@selector(pushManager:openSettingsForNotification:)]) {
        #pragma clang diagnostic push
        #pragma clang diagnostic ignored "-Wpartial-availability"
        [[PushNotificationManager pushManager].delegate pushManager:[PushNotificationManager pushManager] openSettingsForNotification:notification];
        #pragma clang diagnostic pop
    }

    if (_originalNotificationCenterDelegate != nil &&
        _originalNotificationCenterDelegateResponds.openSettingsForNotification) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability-new"
        [_originalNotificationCenterDelegate userNotificationCenter:center
                                        openSettingsForNotification:notification];
#pragma clang diagnostic pop
    }
}

- (NSDictionary*)inboxMessageToDictionary:(NSObject<PWInboxMessageProtocol>*) message {
    NSMutableDictionary* dictionary = [[NSMutableDictionary alloc] init];
    [dictionary setValue:@(message.type) forKey:@"type"];
    [dictionary setValue:[self stringOrEmpty: message.imageUrl] forKey:@"imageUrl"];
    [dictionary setValue:[self stringOrEmpty: message.code] forKey:@"code"];
    [dictionary setValue:[self stringOrEmpty: message.title] forKey:@"title"];
    [dictionary setValue:[self stringOrEmpty: message.message] forKey:@"message"];
    [dictionary setValue:[self stringOrEmpty: [self dateToString:message.sendDate]] forKey:@"sendDate"];
    [dictionary setValue:@(message.isRead) forKey:@"isRead"];
    [dictionary setValue:@(message.isActionPerformed) forKey:@"isActionPerformed"];
    
    NSDictionary* actionParams = [NSDictionary dictionaryWithDictionary:message.actionParams];
    NSData* customData = [actionParams valueForKey:@"u"];
    [dictionary setValue:customData forKey:@"customData"];
    
    NSDictionary* result = [NSDictionary dictionaryWithDictionary:dictionary];
    return result;
}

- (NSString *)stringOrEmpty:(NSString *)string {
    return string != nil ? string : @"";
}

- (NSString*)dateToString:(NSDate*)date {
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]];
    [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZ"];
    return [formatter stringFromDate:date];
}

- (PWIInboxStyle *)inboxStyleForDictionary:(NSDictionary *)styleDictionary {
    PWIInboxStyle *style = [PWIInboxStyle defaultStyle];
    
    NSDictionary *defaultImageDict = styleDictionary[@"defaultImageIcon"];
    
    if (defaultImageDict) {
        style.defaultImageIcon = [RCTConvert UIImage:defaultImageDict];
    }
    
    NSString *dateFormat = styleDictionary[@"dateFormat"];
    
    if (dateFormat) {
        style.dateFormatterBlock = ^NSString *(NSDate *date, NSObject *owner) {
            NSDateFormatter *formatter = [NSDateFormatter new];
            formatter.dateFormat = dateFormat;
            return [formatter stringFromDate:date];
        };
    }
    
    NSDictionary *listErrorImageDict = styleDictionary[@"listErrorImage"];
    
    if (listErrorImageDict) {
        style.listErrorImage = [RCTConvert UIImage:listErrorImageDict];
    }
    
    NSDictionary *listEmptyImageDict = styleDictionary[@"listEmptyImage"];
    
    if (listEmptyImageDict) {
        style.listEmptyImage = [RCTConvert UIImage:listEmptyImageDict];
    }
    
    NSDictionary *unreadImageDict = styleDictionary[@"unreadImage"];
    
    if (unreadImageDict) {
        style.unreadImage = [RCTConvert UIImage:unreadImageDict];
    }
    
    NSString *listErrorMessage = styleDictionary[@"listErrorMessage"];
    
    if (listErrorMessage) {
        style.listErrorMessage = listErrorMessage;
    }
    
    NSString *listEmptyMessage = styleDictionary[@"listEmptyMessage"];
    
    if (listEmptyMessage) {
        style.listEmptyMessage = listEmptyMessage;
    }
    
    NSNumber *accentColorValue = styleDictionary[@"accentColor"];
    
    if (accentColorValue) {
        style.accentColor = [RCTConvert UIColor:accentColorValue];
    }
    
    NSNumber *defaultTextColorValue = styleDictionary[@"defaultTextColor"];
    
    if (defaultTextColorValue) {
        style.defaultTextColor = [RCTConvert UIColor:defaultTextColorValue];
    }
    
    NSNumber *backgroundColorValue = styleDictionary[@"backgroundColor"];
    
    if (backgroundColorValue) {
        style.backgroundColor = [RCTConvert UIColor:backgroundColorValue];
    }
    
    if (accentColorValue) {
        style.accentColor = [RCTConvert UIColor:accentColorValue];
    }
    
    NSNumber *highlightColorValue = styleDictionary[@"highlightColor"];
    
    if (highlightColorValue) {
        style.selectionColor = [RCTConvert UIColor:highlightColorValue];
    }
    
    NSNumber *titleColorValue = styleDictionary[@"titleColor"];
    
    if (titleColorValue) {
        style.titleColor = [RCTConvert UIColor:titleColorValue];
    }
    
    NSNumber *descriptionColorValue = styleDictionary[@"descriptionColor"];
    
    if (descriptionColorValue) {
        style.descriptionColor = [RCTConvert UIColor:descriptionColorValue];
    }
    
    NSNumber *dateColorValue = styleDictionary[@"dateColor"];
    
    if (dateColorValue) {
        style.dateColor = [RCTConvert UIColor:dateColorValue];
    }
    
    NSNumber *dividerColorValue = styleDictionary[@"dividerColor"];
    
    if (dividerColorValue) {
        style.separatorColor = [RCTConvert UIColor:dividerColorValue];
    }
    
    NSNumber *barBackgroundColor = styleDictionary[@"barBackgroundColor"];
    
    if (barBackgroundColor) {
        style.barBackgroundColor = [RCTConvert UIColor:barBackgroundColor];
    }
    
    NSNumber *barAccentColor = styleDictionary[@"barAccentColor"];
    
    if (barAccentColor) {
        style.barAccentColor = [RCTConvert UIColor:barAccentColor];
    }
    
    NSNumber *barTextColor = styleDictionary[@"barTextColor"];
    
    if (barTextColor) {
        style.barTextColor = [RCTConvert UIColor:barTextColor];
    }
    
    return style;
}

- (void)closeInbox {
    UIViewController *topViewController = [PushwooshPlugin findRootViewController];
    if ([topViewController isKindOfClass:[UINavigationController class]] && [((UINavigationController*)topViewController).viewControllers.firstObject isKindOfClass:[PWIInboxViewController class]]) {
        [topViewController dismissViewControllerAnimated:YES completion:nil];
    }
}

+ (UIViewController*)findRootViewController {
    UIApplication *sharedApplication = [UIApplication valueForKey:@"sharedApplication"];
    UIViewController *controller = sharedApplication.keyWindow.rootViewController;
    
    while (controller.presentedViewController) {
        controller = controller.presentedViewController;
    }
    return controller;
}

RCT_EXPORT_METHOD(isCommunicationEnabled:(RCTResponseSenderBlock)callback) {
    BOOL isCommunicationEnabled = [self getCommunicationEnabledState];

    callback(@[@(isCommunicationEnabled)]);
}

RCT_EXPORT_METHOD(setCommunicationEnabled:(BOOL)enabled success:(RCTResponseSenderBlock)successCallback error:(RCTResponseSenderBlock)errorCallback) {
    [[NSUserDefaults standardUserDefaults] setBool:enabled forKey:PW_COMMUNICATION_ENABLED_KEY];
        
    if (enabled) {
        [[Pushwoosh sharedInstance] startServerCommunication];
    } else {
        [[Pushwoosh sharedInstance] stopServerCommunication];
    }

    
    successCallback(@[]);
}

- (BOOL)getCommunicationEnabledState {
    NSBundle *bundle = [NSBundle mainBundle];
    NSObject *plistValue = [bundle objectForInfoDictionaryKey:PW_COMMUNICATION_ENABLED_PLIST_KEY];
    
    if (plistValue != nil) {
        if ([plistValue isKindOfClass:[NSNumber class]]) {
            return [(NSNumber *)plistValue boolValue];
        } else if ([plistValue isKindOfClass:[NSString class]]) {
            NSString *stringValue = [(NSString *)plistValue lowercaseString];
            return [stringValue isEqualToString:@"true"] || [stringValue isEqualToString:@"yes"] || [stringValue isEqualToString:@"1"];
        }
    }

    if ([[NSUserDefaults standardUserDefaults] objectForKey:PW_COMMUNICATION_ENABLED_KEY] != nil) {
        return [[NSUserDefaults standardUserDefaults] boolForKey:PW_COMMUNICATION_ENABLED_KEY];
    }
    
    return YES;
}


RCT_EXPORT_METHOD(createLocalNotification:(NSDictionary *)params){
    NSString *body = params[@"msg"];
    NSUInteger delay = [params[@"seconds"] unsignedIntegerValue];
    NSDictionary *userData = params[@"userData"];

    [self sendLocalNotificationWithBody:body delay:delay userData:userData];
}

- (void)sendLocalNotificationWithBody:(NSString *)body delay:(NSUInteger)delay userData:(NSDictionary *)userData {
    if (@available(iOS 10, *)) {
        UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
        UNMutableNotificationContent *content = [UNMutableNotificationContent new];
        content.body = body;
        content.sound = [UNNotificationSound defaultSound];
        content.userInfo = userData;
        UNTimeIntervalNotificationTrigger *trigger = delay > 0 ? [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:delay repeats:NO] : nil;
        NSString *identifier = @"LocalNotification";
        UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier
                                                                              content:content
                                                                              trigger:trigger];
        
        [center addNotificationRequest:request withCompletionHandler:^(NSError *_Nullable error) {
            if (error != nil) {
                NSLog(@"Something went wrong: %@", error);
            }
        }];
    } else {
        UILocalNotification *localNotification = [[UILocalNotification alloc] init];
        localNotification.fireDate = [NSDate dateWithTimeIntervalSinceNow:delay];
        localNotification.alertBody = body;
        localNotification.timeZone = [NSTimeZone defaultTimeZone];
        localNotification.userInfo = userData;
        [[UIApplication sharedApplication] scheduleLocalNotification:localNotification];
    }
}

RCT_EXPORT_METHOD(clearLocalNotification){
    if (@available(iOS 10, *)) {
        UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
        [center removeAllDeliveredNotifications];
        [center removeAllPendingNotificationRequests];
    } else{
        [[UIApplication sharedApplication] cancelAllLocalNotifications];
    }
}

RCT_EXPORT_METHOD(clearNotificationCenter){
    [PushNotificationManager clearNotificationCenter];
}


RCT_EXPORT_METHOD(setReverseProxy:(NSString *)url headers:(NSDictionary *)headers) {
    [PushwooshConfig setReverseProxy:url headers:headers];
}

RCT_EXPORT_METHOD(enableHuaweiPushNotifications) {
    // available in Android only
}

#pragma mark - Android-only settings

// Declared so the TurboModule spec is fully implemented on iOS: a call from JS is a no-op here
// instead of a TypeError. The settings exist only in the Android SDK.

RCT_EXPORT_METHOD(setMultiNotificationMode:(BOOL)on) {}

RCT_EXPORT_METHOD(setLightScreenOnNotification:(BOOL)on) {}

RCT_EXPORT_METHOD(setEnableLED:(BOOL)on) {}

RCT_EXPORT_METHOD(setColorLED:(double)color) {}

RCT_EXPORT_METHOD(setSoundType:(double)type) {}

RCT_EXPORT_METHOD(setVibrateType:(double)type) {}

RCT_EXPORT_METHOD(setNotificationIconBackgroundColor:(NSString *)color) {}

RCT_EXPORT_METHOD(registerSMSNumber:(NSString *)phoneNumber) {
    [[Pushwoosh sharedInstance] registerSmsNumber:phoneNumber];
}

RCT_EXPORT_METHOD(registerWhatsappNumber:(NSString *)phoneNumber) {
    [[Pushwoosh sharedInstance] registerWhatsappNumber:phoneNumber];
}

RCT_EXPORT_METHOD(setRichMediaType:(double)type) {
    PWRichMediaPresentationStyle style = (NSInteger)type == 0
        ? PWRichMediaPresentationStyleModal
        : PWRichMediaPresentationStyleLegacy;
    [Pushwoosh.media setRichMediaPresentationStyle:style];
}

RCT_EXPORT_METHOD(getRichMediaType:(RCTResponseSenderBlock)callback) {
    PWRichMediaPresentationStyle style = [Pushwoosh.media richMediaPresentationStyle];
    if (callback) {
        callback(@[@(style)]);
    }
}

#pragma mark - PushNotificationDelegate

- (void)onDidRegisterForRemoteNotificationsWithDeviceToken:(NSString *)token {
    [[PWEventDispatcher sharedDispatcher] dispatchEvent:kRegistrationSuccesEvent withArgs:@[ objectOrNull(token) ]];
}

- (void)onDidFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
    [[PWEventDispatcher sharedDispatcher] dispatchEvent:kRegistrationErrorEvent withArgs:@[ objectOrNull([error localizedDescription]) ]];
}

- (void)onPushReceived:(PushNotificationManager *)pushManager withNotification:(NSDictionary *)pushNotification onStart:(BOOL)onStart {
    [[PWEventDispatcher sharedDispatcher] dispatchEvent:kPushReceivedEvent withArgs:@[ objectOrNull(pushNotification) ]];
    
    [self sendJSEvent:kPushReceivedJSEvent withArgs:pushNotification];
}

- (void)onPushAccepted:(PushNotificationManager *)manager withNotification:(NSDictionary *)pushNotification onStart:(BOOL)onStart {
    [[PWEventDispatcher sharedDispatcher] dispatchEvent:kPushOpenEvent withArgs:@[ objectOrNull(pushNotification) ]];
    
    [self sendJSEvent:kPushOpenJSEvent withArgs:pushNotification];
}

#pragma mark - RCTEventEmitter

// RCTEventEmitter withholds events until JS subscribes through NativeEventEmitter, which is what
// raises its listener count. The plugin's public API is DeviceEventEmitter, which never raises it,
// so observation stays off and every event goes out.
- (instancetype)init {
    return [super initWithDisabledObservation];
}

- (void)sendJSEvent:(NSString*)event withArgs:(NSDictionary*)args {
    [self sendEventWithName:event body:args];
}

- (NSArray<NSString *> *)supportedEvents {
    return @[ kPushOpenJSEvent, kPushReceivedJSEvent ];
}

@end

@implementation UIApplication (InternalPushRuntime)

- (BOOL)pushwooshUseRuntimeMagic {
    return YES;
}

// Just keep the launch notification until the module starts and callback functions initalizes
- (void)onPushReceived:(PushNotificationManager *)manager withNotification:(NSDictionary *)pushNotification onStart:(BOOL)onStart {
    if (onStart) {
        gStartPushData = pushNotification;
        gStartPushReplayed = NO;
        pwplugin_rememberWebLaunchLink(pushNotification);
        // Save deep link URL for New Architecture (Linking.getInitialURL support)
        NSURL *deepLink = pwplugin_deepLinkURLFromPushLink(pushNotification[@"l"]);
        if (deepLink) {
            gPushDeepLinkURL = deepLink;
        }
    }
}
- (void)onPushAccepted:(PushNotificationManager *)manager withNotification:(NSDictionary *)pushNotification onStart:(BOOL)onStart {
    if (onStart) {
        gStartPushData = pushNotification;
        gStartPushReplayed = NO;
        pwplugin_rememberWebLaunchLink(pushNotification);
        // Save deep link URL for New Architecture (Linking.getInitialURL support)
        NSURL *deepLink = pwplugin_deepLinkURLFromPushLink(pushNotification[@"l"]);
        if (deepLink) {
            gPushDeepLinkURL = deepLink;
        }
    }
}

- (NSObject<PushNotificationDelegate> *)getPushwooshDelegate {
    return (NSObject<PushNotificationDelegate> *)self;
}

@end

#pragma mark - RCTLinkingManager Swizzle for New Architecture

// Temporary delegate to capture push notification on cold start before React Native initializes
API_AVAILABLE(ios(10.0))
@interface PWEarlyNotificationDelegate : NSObject <UNUserNotificationCenterDelegate>
@property (nonatomic, weak) id<UNUserNotificationCenterDelegate> originalDelegate;
@end

// PLUGIN_NOTIFICATION_HANDLER suppresses the native delegate, and the plugin's own delegate is
// installed in JS init — too late for a cold start, so nobody accepts the launch push. Both early
// hooks below call this; the push hash keeps the second one from repeating it.
static void pwplugin_acceptColdStartPushOnce(NSDictionary *userInfo) {
    if (![userInfo isKindOfClass:[NSDictionary class]]) {
        return;
    }
    if (![[[NSBundle mainBundle] objectForInfoDictionaryKey:kPushwooshPluginImplementationInfoPlistKey] boolValue]) {
        return;
    }
    if (![PWMessage isPushwooshMessage:userInfo]) {
        return;
    }

    NSString *hash = userInfo[@"p"];
    if ([hash isKindOfClass:[NSString class]]) {
        if ([gEarlyHandledHash isEqualToString:hash]) {
            return;
        }
        gEarlyHandledHash = hash;
    }

    [[PushNotificationManager pushManager] handlePushAccepted:userInfo onStart:YES];
}

@implementation PWEarlyNotificationDelegate

- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
         withCompletionHandler:(void (^)(void))completionHandler API_AVAILABLE(ios(10.0)) {
    BOOL dismissed = [response.actionIdentifier isEqualToString:UNNotificationDismissActionIdentifier];
    // After init the plugin's own delegate or the app accepts the tap; a dismissed push opened nothing.
    if (!gPluginInitialized && !dismissed && [response.notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
        NSDictionary *userInfo = response.notification.request.content.userInfo;
        NSURL *appSchemeDeepLink = pwplugin_deepLinkURLFromPushLink(userInfo[@"l"]);
        pwplugin_rememberWebLaunchLink(userInfo);

        if (appSchemeDeepLink) {
            // Routing it through the SDK too would deliver the deep link twice.
            gPushDeepLinkURL = appSchemeDeepLink;
        } else {
            pwplugin_acceptColdStartPushOnce(userInfo);
        }
    }

    // Forward to original delegate
    if (self.originalDelegate && [self.originalDelegate respondsToSelector:@selector(userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:)]) {
        [self.originalDelegate userNotificationCenter:center didReceiveNotificationResponse:response withCompletionHandler:completionHandler];
    } else {
        completionHandler();
    }
}

- (void)userNotificationCenter:(UNUserNotificationCenter *)center
       willPresentNotification:(UNNotification *)notification
         withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler API_AVAILABLE(ios(10.0)) {
    // Forward to original delegate
    if (self.originalDelegate && [self.originalDelegate respondsToSelector:@selector(userNotificationCenter:willPresentNotification:withCompletionHandler:)]) {
        [self.originalDelegate userNotificationCenter:center willPresentNotification:notification withCompletionHandler:completionHandler];
    } else {
        completionHandler(UNNotificationPresentationOptionNone);
    }
}

@end

static PWEarlyNotificationDelegate *gEarlyDelegate = nil;

// Wraps the delegate the app set in didFinishLaunching. A scene-based app gets the launch push's
// response before the main queue drains, so installing it any later loses the push.
static void pwplugin_installEarlyNotificationDelegate(void) {
    if (gEarlyDelegate) {
        return;
    }

    UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
    gEarlyDelegate = [[PWEarlyNotificationDelegate alloc] init];
    gEarlyDelegate.originalDelegate = center.delegate;
    center.delegate = gEarlyDelegate;
}

@implementation RCTLinkingManager (PushwooshDeepLink)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // Swizzle getInitialURL:reject: to support deep links from push on New Architecture
        pushwoosh_swizzle(
            [RCTLinkingManager class],
            @selector(getInitialURL:reject:),
            @selector(pwplugin_original_getInitialURL:reject:),
            (IMP)pwplugin_getInitialURL,
            "v@:@@"
        );

        // RN keeps the notification name file-local; the string is its contract with the app delegate.
        [[NSNotificationCenter defaultCenter] addObserverForName:@"RCTOpenURLNotification"
                                                          object:nil
                                                           queue:nil
                                                      usingBlock:^(NSNotification *notification) {
            pwplugin_handleOpenURLNotification(notification);
        }];

        [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification
                                                          object:nil
                                                           queue:nil
                                                      usingBlock:^(NSNotification *notification) {
            pwplugin_installEarlyNotificationDelegate();

            NSDictionary *launchOptions = notification.userInfo;
            NSDictionary *remoteNotification = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey];
            if (remoteNotification) {
                NSURL *deepLink = pwplugin_deepLinkURLFromPushLink(remoteNotification[@"l"]);
                pwplugin_rememberWebLaunchLink(remoteNotification);
                if (deepLink) {
                    if (!gPushDeepLinkURL) {
                        gPushDeepLinkURL = deepLink;
                    }
                } else {
                    pwplugin_acceptColdStartPushOnce(remoteNotification);
                }
            }
        }];
    });
}

@end
