@import Pendo;


#import "ReactNativePendo.h"

typedef void(^CompletionBlock)(void);

//Enum duplication: the same enum is defined inside ScreenManagerRN but can't be imported here
typedef NS_ENUM(NSInteger, PNDScanRequestReason) {
    PNDScanRequestReasonUnknown = 0,
    PNDScanRequestReasonCapture,
    PNDScanRequestReasonReturnFromAlert,
    PNDScanRequestReasonDynamicElement
};

@interface ReactNativePendo ()
@property (nonatomic) BOOL hasListeners;
@property (nonatomic) CompletionBlock completionBlock;
@property (nonatomic) BOOL shouldWaitForResponseFromJS;
@property (atomic) int scanRequestCounter;
@property (nonatomic) PNDScanRequestReason scanReason; // if scan failure occurs we want to send the reason we requested the scan
@property (nonatomic) BOOL shouldScanDynamicElements;
@property (nonatomic, nullable) id uiManager;
@end

static NSString *const kDebugMode = @"debugMode";
static NSString *const kOptions = @"options";
static NSString *const kReactNativeVersion = @"reactNativeVersion";
static NSString *const kPluginVersion = @"pluginVersion";
static NSString *const kEnvironmentName = @"environmentName";
static NSString *const kVisitorId = @"visitorId";
static NSString *const kAccountId = @"accountId";
static NSString *const kVisitorData = @"visitorData";
static NSString *const kAccountData = @"accountData";
static NSString *const kOnDefaultRequestAnalyzer = @"onDefaultRequestAnalyzer";
static NSString *const kOnDynamicContentRequestAnalyzer = @"onDynamicContentRequestAnalyzer";
static NSString *const kOnCaptureRequestAnalyzer = @"onCaptureRequestAnalyzer";
static NSString *const kOnReturnFromAlertRequestAnalyzer = @"onReturnFromAlertRequestAnalyzer";
static NSString *const kOnScreenContentChange = @"onScreenContentChange";
static NSString *const kOnModalStateVisible = @"onModalStateVisible";
static NSString *const kOnModalStateHidden = @"onModalStateHidden";
static NSString *const kCompletionBlock = @"block";
static NSString *const kScanRequestReason = @"scanReason";
static NSString *const kShouldIgnoreDynamicContent = @"shouldIgnoreDynamicContentRN";
static NSString *const kDynamicScreenScanDebouncerDelay = @"dynamicScreenScanDebouncerDelayRN";

static NSUInteger const kReactNavigation = 2;
static NSUInteger const kExpoRouter = 5;


@implementation ReactNativePendo

RCT_EXPORT_MODULE()

- (instancetype)init {
    if (self = [super init]) {
        _shouldWaitForResponseFromJS = NO;
        _scanRequestCounter = 0;
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(JSHierarchyScan:) name:PNDRequiresJSHierarchyScan object:nil];
    }
    return self;
}

- (void)JSHierarchyScan:(NSNotification *)notification {
    self.scanRequestCounter++;
    self.shouldWaitForResponseFromJS = YES;
    if (self.hasListeners) {
        self.scanReason = [notification.userInfo[kScanRequestReason] integerValue];

        if (self.scanReason == PNDScanRequestReasonDynamicElement && !self.shouldScanDynamicElements) {
            self.shouldWaitForResponseFromJS = NO;
            self.scanRequestCounter--;
            return;
        }

        id block = notification.userInfo[kCompletionBlock];

        if (block != nil) {
            self.completionBlock = (CompletionBlock)block;
        }

        dispatch_async([self methodQueue], ^{
            [self sendEventWithName:[self scanRequestEvent] body:nil];
        });

        // Timeout in case we get no response from JS about the new scan or failure
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), [self methodQueue], ^{
            self.scanRequestCounter--;
            if (self.shouldWaitForResponseFromJS) {
                if (self.scanRequestCounter == 0) {
                    NSDictionary *info = @{ @"errorMessage" : @"timeout - failed to receive screen data from JS in time" };
                    [PendoManagerReactNative sendFailureInfo:info shouldSendErrorToBE:YES scanReason:self.scanReason];
                    self.completionBlock = nil;
                } else {
                    NSString *message = [NSString stringWithFormat:@"Response for JS scan request is ignored due to more recent requests. Scan request counter: %d", self.scanRequestCounter+1];
                    [PendoManagerReactNative logMessage:message];
                }
            }
        });
    }
}

#pragma mark - Facebook Overrides (Event Emitter)
- (void)startObserving {
    self.hasListeners = YES;
}

- (void)stopObserving {
    self.hasListeners = NO;
}

+ (BOOL)requiresMainQueueSetup {
    return YES;
}

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

- (NSArray<NSString *> *)supportedEvents {
    return @[kOnDefaultRequestAnalyzer, kOnReturnFromAlertRequestAnalyzer, kOnCaptureRequestAnalyzer, kOnDynamicContentRequestAnalyzer, kOnScreenContentChange, kOnModalStateHidden, kOnModalStateVisible];
}

- (void)setBridge:(RCTBridge *)bridge {
    [super setBridge:bridge];
    [PendoManagerReactNative setUiManager:bridge.uiManager];
}

#pragma mark - Private
- (NSString *)scanRequestEvent {
    NSString *eventName = nil;
    switch (self.scanReason) {
        case PNDScanRequestReasonCapture: {
            eventName = kOnCaptureRequestAnalyzer;
            break;
        }
        case PNDScanRequestReasonReturnFromAlert: {
            eventName = kOnReturnFromAlertRequestAnalyzer;
            break;
        }
        case PNDScanRequestReasonDynamicElement: {
            eventName = kOnDynamicContentRequestAnalyzer;
            break;
        }
        default:
            eventName = kOnDefaultRequestAnalyzer;
            break;
    }
    return eventName;
}

#pragma mark - Pendo Module
/**
 *  Call this method on the sharedManger with your application key.
 *
 *  @param appKey The app key for your account
 *  @param reactPlugin The react plugin navigation
 *  @param pendoOptions additional options for internal use only, default nil
 */
RCT_EXPORT_METHOD(setup:(NSString *_Nonnull)appKey reactPlugin:(NSUInteger)plugin withOptions:(NSDictionary * _Nullable)pendoOptions) {
    PendoOptions *options = [[PendoOptions alloc] init];
    //SDK Doesn't distinguish between ExpoRouter and ReactNavigation
    if(plugin == kExpoRouter) {
        plugin = kReactNavigation;
    }
    options.reactPlugin = plugin;

    if (pendoOptions != nil) {
        options.platformVersion = [RCTConvert NSString:pendoOptions[kReactNativeVersion]];
        options.environmentName = [RCTConvert NSString:pendoOptions[kEnvironmentName]];
        options.shouldIgnoreDynamicContentRN = [pendoOptions[kShouldIgnoreDynamicContent] boolValue];
        options.dynamicScreenScanDebouncerDelayRN = [RCTConvert NSNumber:pendoOptions[kDynamicScreenScanDebouncerDelay]];
        BOOL isDebugModeEnabled = [pendoOptions[kDebugMode] boolValue];
        if (isDebugModeEnabled) {
            [self setDebugMode:YES];
        }

        NSString * pluginVersion = [RCTConvert NSString:pendoOptions[kPluginVersion]];
        if (pluginVersion != nil) {
            options.pluginVersion = pluginVersion;
        }
    }

    [[PendoManager sharedManager] setup:appKey withOptions:options];
}

/**
 * Must be called <b>after</b> the SDK was setup.
 *
 * @brief start a session with a new visitor.
 * In case a session was already started before, end the previous one and start a new one.
 *
 * @param visitorId The visitor's ID.
 * @param accountId The account's ID.
 * @param visitorData The visitor's data.
 * @param accountData The account's data.
 */
RCT_EXPORT_METHOD(startSession:(NSString *_Nullable)visitorId
                  accountId:(NSString *_Nullable)accountId
                  visitorData:(NSDictionary *_Nullable)visitorData
                  accountData:(NSDictionary *_Nullable)accountData) {
    [[PendoManager sharedManager] startSession:visitorId accountId:accountId visitorData:visitorData accountData:accountData];
}

/**
 * Set a visitor data
 * This data is used by Pendo Mobile for creating audiences or reporting analytics.
 * For instance you might want to provide data on the visitor's age or if the visitor is logged into a service.
 * @code
 * [[PendoManager sharedManager].setVisitorData:@{@"age" :@"15",@"isLoggedIn",true,...}];
 * @endcode
 *
 * @param visitorData Dictionary containing visitor data
 */
RCT_EXPORT_METHOD(setVisitorData:(NSDictionary * _Nonnull)visitorData) {
    [[PendoManager sharedManager] setVisitorData:visitorData];
}

/**
 * Set a account data
 * This data is used by Pendo Mobile for creating audiences or reporting analytics.
 * For instance you might want to provide data on the account's subscription or if the account is active or not.
 * @code
 * [[PendoManager sharedManager].setAccountData:@{@"key1" :@"value1", ...}];
 * @endcode
 *
 * @param accountData Dictionary containing  account data
 */
RCT_EXPORT_METHOD(setAccountData:(NSDictionary * _Nonnull)accountData) {
    [[PendoManager sharedManager] setAccountData:accountData];
}

/**
 * Must be called <b>after</b> the SDK was initialized.
 * @brief Ends the current session. New session will not be started until switchVisitor is called.
 */
RCT_EXPORT_METHOD(endSession) {
    [[PendoManager sharedManager] endSession];
}

/**
 * @brief Pause showing guides during this session
 */
RCT_EXPORT_METHOD(pauseGuides:(BOOL)dismissGuides) {
    [[PendoManager sharedManager] pauseGuides:dismissGuides];
}

/**
 * @brief Resume showing guides during this session
 */
RCT_EXPORT_METHOD(resumeGuides) {
    [[PendoManager sharedManager] resumeGuides];
}

/**
 * @brief Dismiss all visible guides
 */
RCT_EXPORT_METHOD(dismissVisibleGuides) {
    [[PendoManager sharedManager] dismissVisibleGuides];
}

/**
 * @brief Get current visitorId
 */
RCT_REMAP_METHOD(getVisitorId, visitorResolver:(RCTPromiseResolveBlock)resolve visitorRejecter:(RCTPromiseRejectBlock)reject) {
    NSString *visitorId = [[PendoManager sharedManager] visitorId];
    resolve(visitorId);
}

/**
 * @brief Get current accountId
 */
RCT_REMAP_METHOD(getAccountId,accountResolver:(RCTPromiseResolveBlock)resolve accountRejecter:(RCTPromiseRejectBlock)reject) {
    NSString *accountId = [[PendoManager sharedManager] accountId];
    resolve(accountId);
}

/**
 * @brief Get current deviceId
 */
RCT_REMAP_METHOD(getDeviceId, deviceResolver:(RCTPromiseResolveBlock)resolve deviceRejecter:(RCTPromiseRejectBlock)reject) {
    NSString *deviceId = [[PendoManager sharedManager] getDeviceId];
    resolve(deviceId);
}

/**
 * When your application needs to send additional events about actions your users perform.
 *
 * @param event The event name describing the user’s action.
 * @param properties dictionary of event properties (optional).
 * @brief Queue a track eventsfor transmission, optionally including properties as the payload of the event.
 */
RCT_EXPORT_METHOD(track:(NSString * _Nonnull)event properties:(NSDictionary * _Nullable)properties) {
    [[PendoManager sharedManager] track:event properties:properties];
}

RCT_EXPORT_METHOD(screenChanged:(NSString *)screenName rootTags:(NSArray *)rootTags nodes:(NSArray *)clickableNodes info:(NSDictionary *)info) {
    self.shouldWaitForResponseFromJS = NO;
    [PendoManagerReactNative screenChanged:screenName rootTags:rootTags nodes:clickableNodes info:info];
    if (self.completionBlock != nil) {
        self.completionBlock();
        self.completionBlock = nil;
    }
    self.shouldScanDynamicElements = YES;
}

/**
 * @brief When We want to send indication to the native that something went wrong on the JS side
 *
 * @param userInfo a dictionary to pass in the relevant data (i.e error message).
 * @param shouldSendErrorToBE a boolean indicating whether we should inform BE about the error or not.
 */
RCT_EXPORT_METHOD(sendFailureInfo:(NSDictionary *)userInfo shouldSendErrorToBE:(BOOL)shouldSendErrorToBE) {
    self.shouldWaitForResponseFromJS = NO;
    self.completionBlock = nil;
    [PendoManagerReactNative sendFailureInfo:userInfo shouldSendErrorToBE:shouldSendErrorToBE scanReason:self.scanReason];
}

/**
 * @brief set debug mode to true to get log messages
 */
RCT_EXPORT_METHOD(setDebugMode:(BOOL)isDebugEnabled) {
    [[PendoManager sharedManager] setDebugMode:isDebugEnabled];
}

/**
 * @brief Resume showing guides during this session
 */
RCT_EXPORT_METHOD(sendClickAnalytic:(NSString *)nativeID) {
    // This method is not implemented on the iOS native SDK side since it already handles
    // clicks for every element that is recognized as clickable (passed in as using nativeID)
}

RCT_EXPORT_METHOD(shouldScanForDynamicElements:(BOOL)scanDynamicElements) {
    self.shouldScanDynamicElements = scanDynamicElements;
}

RCT_EXPORT_METHOD(screenContentChanged) {
     [self sendEventWithName:kOnScreenContentChange body:nil];
}

RCT_EXPORT_METHOD(modalStateChanged:(BOOL)isVisible) {
}


- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:PNDRequiresJSHierarchyScan object:nil];
}

@end
