//
//  Adjust.java
//  Adjust SDK
//
//  Created by Abdullah Obaied on 19th October 2016.
//  Copyright (c) 2016-Present Adjust GmbH. All rights reserved.
//

package com.adjust.nativemodule;

import android.net.Uri;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.Map.Entry;
import javax.annotation.Nullable;
import org.json.JSONObject;
import org.json.JSONException;
import com.facebook.react.bridge.*;
import com.facebook.react.modules.core.*;
import com.adjust.sdk.*;

public class Adjust extends ReactContextBaseJavaModule implements
                OnAttributionChangedListener,
                OnEventTrackingSucceededListener,
                OnEventTrackingFailedListener,
                OnSessionTrackingSucceededListener,
                OnSessionTrackingFailedListener,
                OnDeferredDeeplinkResponseListener {
    private static String TAG = "AdjustBridge";
    private boolean isAttributionCallbackImplemented;
    private boolean isEventTrackingSucceededCallbackImplemented;
    private boolean isEventTrackingFailedCallbackImplemented;
    private boolean isSessionTrackingSucceededCallbackImplemented;
    private boolean isSessionTrackingFailedCallbackImplemented;
    private boolean isDeferredDeeplinkCallbackImplemented;
    private boolean isDeferredDeeplinkOpeningEnabled = true;

    public Adjust(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @Override
    public String getName() {
        return "Adjust";
    }

    @Override
    public void onAttributionChanged(AdjustAttribution attribution) {
        sendEvent(
            getReactApplicationContext(),
            "adjust_attributionChanged",
            AdjustUtil.attributionToMap(attribution));
    }

    @Override
    public void onEventTrackingSucceeded(AdjustEventSuccess event) {
        sendEvent(
            getReactApplicationContext(),
            "adjust_eventTrackingSucceeded",
            AdjustUtil.eventSuccessToMap(event));
    }

    @Override
    public void onEventTrackingFailed(AdjustEventFailure event) {
        sendEvent(
            getReactApplicationContext(),
            "adjust_eventTrackingFailed",
            AdjustUtil.eventFailureToMap(event));
    }

    @Override
    public void onSessionTrackingSucceeded(AdjustSessionSuccess session) {
        sendEvent(
            getReactApplicationContext(),
            "adjust_sessionTrackingSucceeded",
            AdjustUtil.sessionSuccessToMap(session));
    }

    @Override
    public void onSessionTrackingFailed(AdjustSessionFailure session) {
        sendEvent(
            getReactApplicationContext(),
            "adjust_sessionTrackingFailed",
            AdjustUtil.sessionFailureToMap(session));
    }

    @Override
    public boolean launchReceivedDeeplink(Uri uri) {
        sendEvent(
            getReactApplicationContext(),
            "adjust_deferredDeeplinkReceived",
            AdjustUtil.deferredDeeplinkToMap(uri));
        return this.isDeferredDeeplinkOpeningEnabled;
    }

    // common methods

    @ReactMethod
    public void initSdk(final ReadableMap mapConfig) {
        if (mapConfig == null) {
            return;
        }

        String appToken = null;
        String environment = null;
        String logLevel = null;
        String sdkPrefix = null;
        String processName = null;
        String defaultTracker = null;
        String externalDeviceId = null;
        String preinstallFilePath = null;
        String fbAppId = null;
        int eventDeduplicationIdsMaxSize = -1;
        boolean isSendingInBackgroundEnabled = false;
        boolean isLogLevelSuppress = false;
        boolean isDeferredDeeplinkOpeningEnabled = false;
        boolean isPreinstallTrackingEnabled = false;
        boolean isCostDataInAttributionEnabled = false;
        boolean isPlayStoreKidsComplianceEnabled = false;
        boolean isCoppaComplianceEnabled = false;
        boolean isDeviceIdsReadingOnceEnabled = false;
        boolean isFirstSessionDelayEnabled = false;
        List<Object> urlStrategyDomains = null;
        boolean useSubdomains = false;
        boolean isDataResidency = false;

        // suppress log level
        if (checkKey(mapConfig, "logLevel")) {
            logLevel = mapConfig.getString("logLevel");
            if (logLevel.equals("SUPPRESS")) {
                isLogLevelSuppress = true;
            }
        }

        // app token
        if (checkKey(mapConfig, "appToken")) {
            appToken = mapConfig.getString("appToken");
        }

        // environment
        if (checkKey(mapConfig, "environment")) {
            environment = mapConfig.getString("environment");
        }

        final AdjustConfig adjustConfig = new AdjustConfig(getReactApplicationContext(), appToken, environment, isLogLevelSuppress);

        // log level
        if (checkKey(mapConfig, "logLevel")) {
            logLevel = mapConfig.getString("logLevel");
            if (logLevel.equals("VERBOSE")) {
                adjustConfig.setLogLevel(LogLevel.VERBOSE);
            } else if (logLevel.equals("DEBUG")) {
                adjustConfig.setLogLevel(LogLevel.DEBUG);
            } else if (logLevel.equals("INFO")) {
                adjustConfig.setLogLevel(LogLevel.INFO);
            } else if (logLevel.equals("WARN")) {
                adjustConfig.setLogLevel(LogLevel.WARN);
            } else if (logLevel.equals("ERROR")) {
                adjustConfig.setLogLevel(LogLevel.ERROR);
            } else if (logLevel.equals("ASSERT")) {
                adjustConfig.setLogLevel(LogLevel.ASSERT);
            } else if (logLevel.equals("SUPPRESS")) {
                adjustConfig.setLogLevel(LogLevel.SUPPRESS);
            } else {
                adjustConfig.setLogLevel(LogLevel.INFO);
            }
        }

        // SDK prefix
        if (checkKey(mapConfig, "sdkPrefix")) {
            sdkPrefix = mapConfig.getString("sdkPrefix");
            adjustConfig.setSdkPrefix(sdkPrefix);
        }

        // main process name
        if (checkKey(mapConfig, "processName")) {
            processName = mapConfig.getString("processName");
            adjustConfig.setProcessName(processName);
        }

        // default tracker
        if (checkKey(mapConfig, "defaultTracker")) {
            defaultTracker = mapConfig.getString("defaultTracker");
            adjustConfig.setDefaultTracker(defaultTracker);
        }

        // external device ID
        if (checkKey(mapConfig, "externalDeviceId")) {
            externalDeviceId = mapConfig.getString("externalDeviceId");
            adjustConfig.setExternalDeviceId(externalDeviceId);
        }

        // URL strategy
        if (checkKey(mapConfig, "urlStrategyDomains")
            && checkKey(mapConfig, "useSubdomains")
            && checkKey(mapConfig, "isDataResidency")) {
            urlStrategyDomains = AdjustUtil.toList(mapConfig.getArray("urlStrategyDomains"));
            if (urlStrategyDomains != null) {
                List<String> strUrlStrategyDomains = new ArrayList<>();
                for (int i = 0; i < urlStrategyDomains.size(); i += 1) {
                    strUrlStrategyDomains.add(urlStrategyDomains.get(i).toString());
                }
                useSubdomains = mapConfig.getBoolean("useSubdomains");
                isDataResidency = mapConfig.getBoolean("isDataResidency");
                adjustConfig.setUrlStrategy(strUrlStrategyDomains, useSubdomains, isDataResidency);
            }
        }
       
        // preinstall file path
        if (checkKey(mapConfig, "preinstallFilePath")) {
            preinstallFilePath = mapConfig.getString("preinstallFilePath");
            adjustConfig.setPreinstallFilePath(preinstallFilePath);
        }

        // FB app ID (META install referrer)
        if (checkKey(mapConfig, "fbAppId")) {
            fbAppId = mapConfig.getString("fbAppId");
            adjustConfig.setFbAppId(fbAppId);
        }

        // sending in background
        if (checkKey(mapConfig, "isSendingInBackgroundEnabled")) {
            isSendingInBackgroundEnabled = mapConfig.getBoolean("isSendingInBackgroundEnabled");
            if (isSendingInBackgroundEnabled) {
               adjustConfig.enableSendingInBackground();
            }
        }

        // preinstall tracking
        if (checkKey(mapConfig, "isPreinstallTrackingEnabled")) {
            isPreinstallTrackingEnabled = mapConfig.getBoolean("isPreinstallTrackingEnabled");
            if(isPreinstallTrackingEnabled) {
               adjustConfig.enablePreinstallTracking();
            }
        }

        // cost data in attribution
        if (checkKey(mapConfig, "isCostDataInAttributionEnabled")) {
            isCostDataInAttributionEnabled = mapConfig.getBoolean("isCostDataInAttributionEnabled");
            if (isCostDataInAttributionEnabled) {
               adjustConfig.enableCostDataInAttribution();
            }
        }

        // play store kids apps compliance
        if (checkKey(mapConfig, "isPlayStoreKidsComplianceEnabled")) {
            isPlayStoreKidsComplianceEnabled = mapConfig.getBoolean("isPlayStoreKidsComplianceEnabled");
            if (isPlayStoreKidsComplianceEnabled) {
               adjustConfig.enablePlayStoreKidsCompliance();
            }
        }

        // should open deferred deep link
        if (checkKey(mapConfig, "isDeferredDeeplinkOpeningEnabled")) {
            isDeferredDeeplinkOpeningEnabled = mapConfig.getBoolean("isDeferredDeeplinkOpeningEnabled");
            this.isDeferredDeeplinkOpeningEnabled = isDeferredDeeplinkOpeningEnabled;
        }

        // COPPA compliance
        if (checkKey(mapConfig, "isCoppaComplianceEnabled")) {
            isCoppaComplianceEnabled = mapConfig.getBoolean("isCoppaComplianceEnabled");
            if (isCoppaComplianceEnabled) {
               adjustConfig.enableCoppaCompliance();
            }
        }

        // read device info only once
        if (checkKey(mapConfig, "isDeviceIdsReadingOnceEnabled")) {
            isDeviceIdsReadingOnceEnabled = mapConfig.getBoolean("isDeviceIdsReadingOnceEnabled");
            if (isDeviceIdsReadingOnceEnabled) {
                adjustConfig.enableDeviceIdsReadingOnce();
            }
        }

        // first session delay
        if (checkKey(mapConfig, "isFirstSessionDelayEnabled")) {
            isFirstSessionDelayEnabled = mapConfig.getBoolean("isFirstSessionDelayEnabled");
            if (isFirstSessionDelayEnabled) {
               adjustConfig.enableFirstSessionDelay();
            }
        }

        // store info 
        if (checkKey(mapConfig, "storeInfo")) {
            ReadableMap storeInfo = mapConfig.getMap("storeInfo");
            try {
                String strStoreInfo = storeInfo.toString();
                JSONObject storeInfoJson = new JSONObject(strStoreInfo);
                String storeName = storeInfoJson.optString("storeName", null);
                if (AdjustUtil.isFieldValid(storeName)) {
                    AdjustStoreInfo adjustStoreInfo = new AdjustStoreInfo(storeName);
                    String storeAppId = storeInfoJson.optString("storeAppId", null);
                    if (AdjustUtil.isFieldValid(storeAppId)) {
                        adjustStoreInfo.setStoreAppId(storeAppId);
                    }
                    adjustConfig.setStoreInfo(adjustStoreInfo);
                }
            } catch (JSONException e) {}
        }

        // max number of deduplication IDs
        if (checkKey(mapConfig, "eventDeduplicationIdsMaxSize")) {
            eventDeduplicationIdsMaxSize = mapConfig.getInt("eventDeduplicationIdsMaxSize");
            adjustConfig.setEventDeduplicationIdsMaxSize(eventDeduplicationIdsMaxSize);
        }

        // attribution callback
        if (isAttributionCallbackImplemented) {
            adjustConfig.setOnAttributionChangedListener(this);
        }

        // event tracking succeeded callback
        if (isEventTrackingSucceededCallbackImplemented) {
            adjustConfig.setOnEventTrackingSucceededListener(this);
        }

        // event tracking failed callback
        if (isEventTrackingFailedCallbackImplemented) {
            adjustConfig.setOnEventTrackingFailedListener(this);
        }

        // session tracking succeeded callback
        if (isSessionTrackingSucceededCallbackImplemented) {
            adjustConfig.setOnSessionTrackingSucceededListener(this);
        }

        // session tracking failed callback
        if (isSessionTrackingFailedCallbackImplemented) {
            adjustConfig.setOnSessionTrackingFailedListener(this);
        }

        // deferred deeplink callback
        if (isDeferredDeeplinkCallbackImplemented) {
            adjustConfig.setOnDeferredDeeplinkResponseListener(this);
        }

        // initialize SDK
        com.adjust.sdk.Adjust.initSdk(adjustConfig);
    }

    @ReactMethod
    public void trackEvent(final ReadableMap mapEvent) {
        if (mapEvent == null) {
            return;
        }

        double revenue = -1.0;
        String eventToken = null;
        String currency = null;
        String transactionId = null;
        String productId = null;
        String purchaseToken = null;
        String callbackId = null;
        String deduplicationId = null;

        List<Object> callbackParameters = null;
        List<Object> partnerParameters = null;

        // event token
        if (checkKey(mapEvent, "eventToken")) {
            eventToken = mapEvent.getString("eventToken");
        }

        final AdjustEvent event = new AdjustEvent(eventToken);

        // revenue and currency
        if (checkKey(mapEvent, "revenue") || checkKey(mapEvent, "currency")) {
            revenue = mapEvent.getDouble("revenue");
            currency = mapEvent.getString("currency");
            event.setRevenue(revenue, currency);
        }

        // callback parameters
        if (checkKey(mapEvent, "callbackParameters")) {
            callbackParameters = AdjustUtil.toList(mapEvent.getArray("callbackParameters"));
            if (null != callbackParameters) {
                for (int i = 0; i < callbackParameters.size(); i += 2) {
                    event.addCallbackParameter(
                        callbackParameters.get(i).toString(),
                        callbackParameters.get(i+1).toString());
                }
            }
        }

        // partner parameters
        if (checkKey(mapEvent, "partnerParameters")) {
            partnerParameters = AdjustUtil.toList(mapEvent.getArray("partnerParameters"));
            if (null != partnerParameters) {
                for (int i = 0; i < partnerParameters.size(); i += 2) {
                    event.addPartnerParameter(
                        partnerParameters.get(i).toString(),
                        partnerParameters.get(i+1).toString());
                }
            }
        }

        // transaction ID
        if (checkKey(mapEvent, "transactionId")) {
            transactionId = mapEvent.getString("transactionId");
            if (null != transactionId) {
                event.setOrderId(transactionId);
            }
        }

        // callback ID
        if (checkKey(mapEvent, "callbackId")) {
            callbackId = mapEvent.getString("callbackId");
            if (null != callbackId) {
                event.setCallbackId(callbackId);
            }
        }

        // product ID
        if (checkKey(mapEvent, "productId")) {
            productId = mapEvent.getString("productId");
            if (null != productId) {
                event.setProductId(productId);
            }
        }

        // purchase token
        if (checkKey(mapEvent, "purchaseToken")) {
            purchaseToken = mapEvent.getString("purchaseToken");
            if (null != purchaseToken) {
                event.setPurchaseToken(purchaseToken);
            }
        }

         // event deduplication
        if (checkKey(mapEvent, "deduplicationId")) {
            deduplicationId = mapEvent.getString("deduplicationId");
            if (null != deduplicationId) {
                event.setDeduplicationId(deduplicationId);
            }
        }

        // track event
        com.adjust.sdk.Adjust.trackEvent(event);
    }

    @ReactMethod
    public void trackAdRevenue(final ReadableMap mapAdRevenue) {
        if (mapAdRevenue == null) {
            return;
        }

        double revenue = -1.0;
        int adImpressionsCount = -1;
        String source = null;
        String currency = null;
        String adRevenueNetwork = null;
        String adRevenueUnit = null;
        String adRevenuePlacement = null;
        List<Object> callbackParameters = null;
        List<Object> partnerParameters = null;

        // ad revenue source
        if (checkKey(mapAdRevenue, "source")) {
            source = mapAdRevenue.getString("source");
        }

        final AdjustAdRevenue adRevenue = new AdjustAdRevenue(source);

        // revenue and currency
        if (checkKey(mapAdRevenue, "revenue") || checkKey(mapAdRevenue, "currency")) {
            revenue = mapAdRevenue.getDouble("revenue");
            currency = mapAdRevenue.getString("currency");
            adRevenue.setRevenue(revenue, currency);
        }

        // ad impressions count
        if (checkKey(mapAdRevenue, "adImpressionsCount")) {
            adImpressionsCount = mapAdRevenue.getInt("adImpressionsCount");
            adRevenue.setAdImpressionsCount(adImpressionsCount);
        }

        // ad revenue network
        if (checkKey(mapAdRevenue, "adRevenueNetwork")) {
            adRevenueNetwork = mapAdRevenue.getString("adRevenueNetwork");
            if (null != adRevenueNetwork) {
                adRevenue.setAdRevenueNetwork(adRevenueNetwork);
            }
        }

        // ad revenue unit
        if (checkKey(mapAdRevenue, "adRevenueUnit")) {
            adRevenueUnit = mapAdRevenue.getString("adRevenueUnit");
            if (null != adRevenueUnit) {
                adRevenue.setAdRevenueUnit(adRevenueUnit);
            }
        }

        // ad revenue placement
        if (checkKey(mapAdRevenue, "adRevenuePlacement")) {
            adRevenuePlacement = mapAdRevenue.getString("adRevenuePlacement");
            if (null != adRevenuePlacement) {
                adRevenue.setAdRevenuePlacement(adRevenuePlacement);
            }
        }

        // callback parameters
        if (checkKey(mapAdRevenue, "callbackParameters")) {
            callbackParameters = AdjustUtil.toList(mapAdRevenue.getArray("callbackParameters"));
            if (null != callbackParameters) {
                for (int i = 0; i < callbackParameters.size(); i += 2) {
                    adRevenue.addCallbackParameter(
                        callbackParameters.get(i).toString(),
                        callbackParameters.get(i+1).toString());
                }
            }
        }

        // partner parameters
        if (checkKey(mapAdRevenue, "partnerParameters")) {
            partnerParameters = AdjustUtil.toList(mapAdRevenue.getArray("partnerParameters"));
            if (null != partnerParameters) {
                for (int i = 0; i < partnerParameters.size(); i += 2) {
                    adRevenue.addPartnerParameter(
                        partnerParameters.get(i).toString(),
                        partnerParameters.get(i+1).toString());
                }
            }
        }

        // track ad revenue
        com.adjust.sdk.Adjust.trackAdRevenue(adRevenue);
    }

    @ReactMethod
    public void trackThirdPartySharing(final ReadableMap mapThirdPartySharing) {
        if (mapThirdPartySharing == null) {
            return;
        }

        Boolean isEnabled = null;
        List<Object> granularOptions = null;
        List<Object> partnerSharingSettings = null;

        // is third party sharing enabled
        if (checkKey(mapThirdPartySharing, "isEnabled")) {
            isEnabled = mapThirdPartySharing.getBoolean("isEnabled");
        }

        final AdjustThirdPartySharing thirdPartySharing = new AdjustThirdPartySharing(isEnabled);

        // granular options
        if (checkKey(mapThirdPartySharing, "granularOptions")) {
            granularOptions = AdjustUtil.toList(mapThirdPartySharing.getArray("granularOptions"));
            if (null != granularOptions) {
                for (int i = 0; i < granularOptions.size(); i += 3) {
                    thirdPartySharing.addGranularOption(
                        granularOptions.get(i).toString(),
                        granularOptions.get(i+1).toString(),
                        granularOptions.get(i+2).toString());
                }
            }
        }

        // partner sharing settings
        if (checkKey(mapThirdPartySharing, "partnerSharingSettings")) {
            partnerSharingSettings = AdjustUtil.toList(mapThirdPartySharing.getArray("partnerSharingSettings"));
            if (null != partnerSharingSettings) {
                for (int i = 0; i < partnerSharingSettings.size(); i += 3) {
                    thirdPartySharing.addPartnerSharingSetting(
                        partnerSharingSettings.get(i).toString(),
                        partnerSharingSettings.get(i+1).toString(),
                        Boolean.parseBoolean(partnerSharingSettings.get(i+2).toString()));
                }
            }
        }

        // track third party sharing
        com.adjust.sdk.Adjust.trackThirdPartySharing(thirdPartySharing);
    }

    @ReactMethod
    public void trackMeasurementConsent(final boolean measurementConsent) {
        com.adjust.sdk.Adjust.trackMeasurementConsent(measurementConsent);
    }

    @ReactMethod
    public void processDeeplink(final ReadableMap mapDeeplink) {
        if (mapDeeplink == null) {
            return;
        }
        if (!checkKey(mapDeeplink, "deeplink")) {
            return;
        }

        String deeplink = mapDeeplink.getString("deeplink");
        AdjustDeeplink adjustDeeplink = new AdjustDeeplink(Uri.parse(deeplink));
        if (checkKey(mapDeeplink, "referrer")) {
            String referrer = mapDeeplink.getString("referrer");
            adjustDeeplink.setReferrer(Uri.parse(referrer));
        }

        com.adjust.sdk.Adjust.processDeeplink(adjustDeeplink, getReactApplicationContext());
    }

    @ReactMethod
    public void processAndResolveDeeplink(final ReadableMap mapDeeplink, final Callback callback) {
        if (mapDeeplink == null) {
            return;
        }
        if (!checkKey(mapDeeplink, "deeplink")) {
            return;
        }

        String deeplink = mapDeeplink.getString("deeplink");
        AdjustDeeplink adjustDeeplink = new AdjustDeeplink(Uri.parse(deeplink));
        if (checkKey(mapDeeplink, "referrer")) {
            String referrer = mapDeeplink.getString("referrer");
            adjustDeeplink.setReferrer(Uri.parse(referrer));
        }

        com.adjust.sdk.Adjust.processAndResolveDeeplink(
            adjustDeeplink,
            getReactApplicationContext(),
            new OnDeeplinkResolvedListener() {
            @Override
            public void onDeeplinkResolved(String resolvedLink) {
                callback.invoke(resolvedLink);
            }
        });
    }

    @ReactMethod
    public void setPushToken(final String token) {
        com.adjust.sdk.Adjust.setPushToken(token, getReactApplicationContext());
    }

    @ReactMethod
    public void gdprForgetMe() {
        com.adjust.sdk.Adjust.gdprForgetMe(getReactApplicationContext());
    }

    @ReactMethod
    public void enable() {
        com.adjust.sdk.Adjust.enable();
    }

    @ReactMethod
    public void disable() {
        com.adjust.sdk.Adjust.disable();
    }

    @ReactMethod
    public void switchToOfflineMode() {
        com.adjust.sdk.Adjust.switchToOfflineMode();
    }

    @ReactMethod
    public void switchBackToOnlineMode() {
        com.adjust.sdk.Adjust.switchBackToOnlineMode();
    }

    @ReactMethod
    public void addGlobalCallbackParameter(final String key, final String value) {
        com.adjust.sdk.Adjust.addGlobalCallbackParameter(key, value);
    }

    @ReactMethod
    public void addGlobalPartnerParameter(final String key, final String value) {
        com.adjust.sdk.Adjust.addGlobalPartnerParameter(key, value);
    }

    @ReactMethod
    public void removeGlobalCallbackParameter(final String key) {
        com.adjust.sdk.Adjust.removeGlobalCallbackParameter(key);
    }

    @ReactMethod
    public void removeGlobalPartnerParameter(final String key) {
        com.adjust.sdk.Adjust.removeGlobalPartnerParameter(key);
    }

    @ReactMethod
    public void removeGlobalCallbackParameters() {
        com.adjust.sdk.Adjust.removeGlobalCallbackParameters();
    }

    @ReactMethod
    public void removeGlobalPartnerParameters() {
        com.adjust.sdk.Adjust.removeGlobalPartnerParameters();
    }

    @ReactMethod
    public void endFirstSessionDelay() {
        com.adjust.sdk.Adjust.endFirstSessionDelay();
    }

    @ReactMethod
    public void enableCoppaComplianceInDelay() {
        com.adjust.sdk.Adjust.enableCoppaComplianceInDelay();
    }

    @ReactMethod
    public void disableCoppaComplianceInDelay() {
        com.adjust.sdk.Adjust.disableCoppaComplianceInDelay();
    }

    @ReactMethod
    public void setExternalDeviceIdInDelay(final String externalDeviceId) {
        com.adjust.sdk.Adjust.setExternalDeviceIdInDelay(externalDeviceId);
    }

    @ReactMethod
    public void isEnabled(final Callback callback) {
        com.adjust.sdk.Adjust.isEnabled(
            getReactApplicationContext(),
            new com.adjust.sdk.OnIsEnabledListener() {
            @Override
            public void onIsEnabledRead(boolean isEnabled) {
                callback.invoke(isEnabled);
            }
        });
    }

    @ReactMethod
    public void getAttribution(final Callback callback) {
        com.adjust.sdk.Adjust.getAttribution(new com.adjust.sdk.OnAttributionReadListener() {
            @Override
            public void onAttributionRead(AdjustAttribution attribution) {
                callback.invoke(AdjustUtil.attributionToMap(attribution));
            }
        });
    }

    @ReactMethod
    public void getAdid(final Callback callback) {
        com.adjust.sdk.Adjust.getAdid(new com.adjust.sdk.OnAdidReadListener() {
            @Override
            public void onAdidRead(String adid) {
                callback.invoke(adid);
            }
        });
    }

    @ReactMethod
    public void getLastDeeplink(final Callback callback) {
        com.adjust.sdk.Adjust.getLastDeeplink(
            getReactApplicationContext(),
            new OnLastDeeplinkReadListener() {
            @Override
            public void onLastDeeplinkRead(Uri uri) {
                String strUri = (uri != null) ? uri.toString() : "";
                callback.invoke(strUri);
            }
        });
    }

    @ReactMethod
    public void getSdkVersion(final String sdkPrefix, final Callback callback) {
        com.adjust.sdk.Adjust.getSdkVersion(new com.adjust.sdk.OnSdkVersionReadListener() {
            @Override
            public void onSdkVersionRead(String sdkVersion) {
                if (sdkVersion == null) {
                    callback.invoke("");
                } else {
                    callback.invoke(sdkPrefix + "@" + sdkVersion);
                }
            }
        });
    }

    @ReactMethod
    public void setAttributionCallbackImplemented() {
        this.isAttributionCallbackImplemented = true;
    }

    @ReactMethod
    public void setEventTrackingSucceededCallbackImplemented() {
        this.isEventTrackingSucceededCallbackImplemented = true;
    }

    @ReactMethod
    public void setEventTrackingFailedCallbackImplemented() {
        this.isEventTrackingFailedCallbackImplemented = true;
    }

    @ReactMethod
    public void setSessionTrackingSucceededCallbackImplemented() {
        this.isSessionTrackingSucceededCallbackImplemented = true;
    }

    @ReactMethod
    public void setSessionTrackingFailedCallbackImplemented() {
        this.isSessionTrackingFailedCallbackImplemented = true;
    }

    @ReactMethod
    public void setDeferredDeeplinkCallbackImplemented() {
        this.isDeferredDeeplinkCallbackImplemented = true;
    }
    
    // android only methods

    @ReactMethod
    public void trackPlayStoreSubscription(final ReadableMap mapEvent) {
        if (mapEvent == null) {
            return;
        }

        int price = -1;
        String currency = null;
        String sku = null;
        String orderId = null;
        String signature = null;
        String purchaseToken = null;
        int purchaseTime = -1;
        List<Object> callbackParameters = null;
        List<Object> partnerParameters = null;

        // price
        if (checkKey(mapEvent, "price")) {
            price = mapEvent.getInt("price");
        }

        // currency
        if (checkKey(mapEvent, "currency")) {
            currency = mapEvent.getString("currency");
        }

        // SKU
        if (checkKey(mapEvent, "sku")) {
            sku = mapEvent.getString("sku");
        }

        // order ID
        if (checkKey(mapEvent, "orderId")) {
            orderId = mapEvent.getString("orderId");
        }

        // signature
        if (checkKey(mapEvent, "signature")) {
            signature = mapEvent.getString("signature");
        }

        // purchase token
        if (checkKey(mapEvent, "purchaseToken")) {
            purchaseToken = mapEvent.getString("purchaseToken");
        }

        AdjustPlayStoreSubscription subscription = new AdjustPlayStoreSubscription(
                price,
                currency,
                sku,
                orderId,
                signature,
                purchaseToken);

        // purchase time
        if (checkKey(mapEvent, "purchaseTime")) {
            purchaseTime = mapEvent.getInt("purchaseTime");
            subscription.setPurchaseTime(purchaseTime);
        }

        // callback parameters
        if (checkKey(mapEvent, "callbackParameters")) {
            callbackParameters = AdjustUtil.toList(mapEvent.getArray("callbackParameters"));
            if (null != callbackParameters) {
                for (int i = 0; i < callbackParameters.size(); i += 2) {
                    subscription.addCallbackParameter(
                        callbackParameters.get(i).toString(),
                        callbackParameters.get(i+1).toString());
                }
            }
        }

        // partner parameters
        if (checkKey(mapEvent, "partnerParameters")) {
            partnerParameters = AdjustUtil.toList(mapEvent.getArray("partnerParameters"));
            if (null != partnerParameters) {
                for (int i = 0; i < partnerParameters.size(); i += 2) {
                    subscription.addPartnerParameter(
                        partnerParameters.get(i).toString(),
                        partnerParameters.get(i+1).toString());
                }
            }
        }

        // track subscription
        com.adjust.sdk.Adjust.trackPlayStoreSubscription(subscription);
    }

    @ReactMethod
    public void verifyPlayStorePurchase(final ReadableMap mapEvent, final Callback callback) {
        if (mapEvent == null) {
            return;
        }

        String productId = null;
        String purchaseToken = null;

        // product ID
        if (checkKey(mapEvent, "productId")) {
            productId = mapEvent.getString("productId");
        }

        // purchase token
        if (checkKey(mapEvent, "purchaseToken")) {
            purchaseToken = mapEvent.getString("purchaseToken");
        }

        final AdjustPlayStorePurchase purchase = new AdjustPlayStorePurchase(productId, purchaseToken);

        // verify purchase
        com.adjust.sdk.Adjust.verifyPlayStorePurchase(purchase, new OnPurchaseVerificationFinishedListener() {
            @Override
            public void onVerificationFinished(AdjustPurchaseVerificationResult verificationResult) {
                if (callback != null) {
                    WritableMap map = Arguments.createMap();
                    if (null == verificationResult) {
                        callback.invoke(map);
                        return;
                    }
                    map.putString(
                        AdjustUtil.PURCHASE_VERIFICATION_STATUS,
                        null != verificationResult.getVerificationStatus() ? verificationResult.getVerificationStatus() : "");
                    map.putString(
                        AdjustUtil.PURCHASE_VERIFICATION_CODE,
                        String.valueOf(verificationResult.getCode()));
                    map.putString(AdjustUtil.PURCHASE_VERIFICATION_MESSAGE,
                        null != verificationResult.getMessage() ? verificationResult.getMessage() : "");
                    callback.invoke(map);
                }
            }
        });
    }

    @ReactMethod
    public void verifyAndTrackPlayStorePurchase(final ReadableMap mapEvent, final Callback callback) {
        if (mapEvent == null) {
            return;
        }

        double revenue = -1.0;
        String eventToken = null;
        String currency = null;
        String transactionId = null;
        String productId = null;
        String purchaseToken = null;
        String callbackId = null;
        String deduplicationId = null;

        List<Object> callbackParameters = null;
        List<Object> partnerParameters = null;

        // event token
        if (checkKey(mapEvent, "eventToken")) {
            eventToken = mapEvent.getString("eventToken");
        }

        final AdjustEvent event = new AdjustEvent(eventToken);

        // revenue and currency
        if (checkKey(mapEvent, "revenue") || checkKey(mapEvent, "currency")) {
            revenue = mapEvent.getDouble("revenue");
            currency = mapEvent.getString("currency");
            event.setRevenue(revenue, currency);
        }

        // callback parameters
        if (checkKey(mapEvent, "callbackParameters")) {
            callbackParameters = AdjustUtil.toList(mapEvent.getArray("callbackParameters"));
            if (null != callbackParameters) {
                for (int i = 0; i < callbackParameters.size(); i += 2) {
                    event.addCallbackParameter(
                        callbackParameters.get(i).toString(),
                        callbackParameters.get(i+1).toString());
                }
            }
        }

        // partner parameters
        if (checkKey(mapEvent, "partnerParameters")) {
            partnerParameters = AdjustUtil.toList(mapEvent.getArray("partnerParameters"));
            if (null != partnerParameters) {
                for (int i = 0; i < partnerParameters.size(); i += 2) {
                    event.addPartnerParameter(
                        partnerParameters.get(i).toString(),
                        partnerParameters.get(i+1).toString());
                }
            }
        }

        // transaction ID
        if (checkKey(mapEvent, "transactionId")) {
            transactionId = mapEvent.getString("transactionId");
            if (null != transactionId) {
                event.setOrderId(transactionId);
            }
        }

        // callback ID
        if (checkKey(mapEvent, "callbackId")) {
            callbackId = mapEvent.getString("callbackId");
            if (null != callbackId) {
                event.setCallbackId(callbackId);
            }
        }

        // product ID
        if (checkKey(mapEvent, "productId")) {
            productId = mapEvent.getString("productId");
            if (null != productId) {
                event.setProductId(productId);
            }
        }

        // purchase token
        if (checkKey(mapEvent, "purchaseToken")) {
            purchaseToken = mapEvent.getString("purchaseToken");
            if (null != purchaseToken) {
                event.setPurchaseToken(purchaseToken);
            }
        }

         // event deduplication
        if (checkKey(mapEvent, "deduplicationId")) {
            deduplicationId = mapEvent.getString("deduplicationId");
            if (null != deduplicationId) {
                event.setDeduplicationId(deduplicationId);
            }
        }

        // verify and track purrchase
        com.adjust.sdk.Adjust.verifyAndTrackPlayStorePurchase(event, new OnPurchaseVerificationFinishedListener() {
            @Override
            public void onVerificationFinished(AdjustPurchaseVerificationResult verificationResult) {
                if (callback != null) {
                    WritableMap map = Arguments.createMap();
                    if (null == verificationResult) {
                        callback.invoke(map);
                        return;
                    }
                    map.putString(
                        AdjustUtil.PURCHASE_VERIFICATION_STATUS,
                        null != verificationResult.getVerificationStatus() ? verificationResult.getVerificationStatus() : "");
                    map.putString(
                        AdjustUtil.PURCHASE_VERIFICATION_CODE,
                        String.valueOf(verificationResult.getCode()));
                    map.putString(AdjustUtil.PURCHASE_VERIFICATION_MESSAGE,
                        null != verificationResult.getMessage() ? verificationResult.getMessage() : "");
                    callback.invoke(map);
                }
            }
        });
    }

    @ReactMethod
    public void enablePlayStoreKidsComplianceInDelay() {
        com.adjust.sdk.Adjust.enablePlayStoreKidsComplianceInDelay();
    }

    @ReactMethod
    public void disablePlayStoreKidsComplianceInDelay() {
        com.adjust.sdk.Adjust.disablePlayStoreKidsComplianceInDelay();
    }

    @ReactMethod
    public void getGoogleAdId(final Callback callback) {
        com.adjust.sdk.Adjust.getGoogleAdId(
            getReactApplicationContext(),
            new com.adjust.sdk.OnGoogleAdIdReadListener() {
            @Override
            public void onGoogleAdIdRead(String googleAdId) {
                callback.invoke(googleAdId);
            }
        });
    }

    @ReactMethod
    public void getAmazonAdId(final Callback callback) {
        com.adjust.sdk.Adjust.getAmazonAdId(
            getReactApplicationContext(),
            new com.adjust.sdk.OnAmazonAdIdReadListener() {
            @Override
            public void onAmazonAdIdRead(String amazonAdId) {
                callback.invoke(amazonAdId);
            }
        });
    }

    // testing only

    @ReactMethod
    public void onResume() {
        com.adjust.sdk.Adjust.onResume();
    }

    @ReactMethod
    public void onPause() {
        com.adjust.sdk.Adjust.onPause();
    }

    @ReactMethod
    public void setTestOptions(final ReadableMap mapTest) {
        if (mapTest == null) {
            return;
        }

        final AdjustTestOptions testOptions = new AdjustTestOptions();
        if (checkKey(mapTest, "baseUrl")) {
            String value = mapTest.getString("baseUrl");
            testOptions.baseUrl = value;
        }
        if (checkKey(mapTest, "gdprUrl")) {
            String value = mapTest.getString("gdprUrl");
            testOptions.gdprUrl = value;
        }
        if (checkKey(mapTest, "subscriptionUrl")) {
            String value = mapTest.getString("subscriptionUrl");
            testOptions.subscriptionUrl = value;
        }
        if (checkKey(mapTest, "purchaseVerificationUrl")) {
            String value = mapTest.getString("purchaseVerificationUrl");
            testOptions.purchaseVerificationUrl = value;
        }
        if (checkKey(mapTest, "basePath")) {
            String value = mapTest.getString("basePath");
            testOptions.basePath = value;
        }
        if (checkKey(mapTest, "gdprPath")) {
            String value = mapTest.getString("gdprPath");
            testOptions.gdprPath = value;
        }
        if (checkKey(mapTest, "subscriptionPath")) {
            String value = mapTest.getString("subscriptionPath");
            testOptions.subscriptionPath = value;
        }
        if (checkKey(mapTest, "purchaseVerificationPath")) {
            String value = mapTest.getString("purchaseVerificationPath");
            testOptions.purchaseVerificationPath = value;
        }
        if (checkKey(mapTest, "timerIntervalInMilliseconds")) {
            try {
                Long value = Long.parseLong(mapTest.getString("timerIntervalInMilliseconds"));
                testOptions.timerIntervalInMilliseconds = value;
            } catch (NumberFormatException ex) {
                ex.printStackTrace();
                Log.d(TAG, "Can't format number");
            }
        }
        if (checkKey(mapTest, "timerStartInMilliseconds")) {
            try {
                Long value = Long.parseLong(mapTest.getString("timerStartInMilliseconds"));
                testOptions.timerStartInMilliseconds = value;
            } catch (NumberFormatException ex) {
                ex.printStackTrace();
                Log.d(TAG, "Can't format number");
            }
        }
        if (checkKey(mapTest, "sessionIntervalInMilliseconds")) {
            try {
                Long value = Long.parseLong(mapTest.getString("sessionIntervalInMilliseconds"));
                testOptions.sessionIntervalInMilliseconds = value;
            } catch (NumberFormatException ex) {
                ex.printStackTrace();
                Log.d(TAG, "Can't format number");
            }
        }
        if (checkKey(mapTest, "subsessionIntervalInMilliseconds")) {
            try {
                Long value = Long.parseLong(mapTest.getString("subsessionIntervalInMilliseconds"));
                testOptions.subsessionIntervalInMilliseconds = value;
            } catch (NumberFormatException ex) {
                ex.printStackTrace();
                Log.d(TAG, "Can't format number");
            }
        }
        if (checkKey(mapTest, "noBackoffWait")) {
            boolean value = mapTest.getBoolean("noBackoffWait");
            testOptions.noBackoffWait = value;
        }
        if (checkKey(mapTest, "teardown")) {
            boolean value = mapTest.getBoolean("teardown");
            testOptions.teardown = value;
        }
        if (checkKey(mapTest, "deleteState")) {
            boolean value = mapTest.getBoolean("deleteState");
            if (value == true) {
                testOptions.context = getReactApplicationContext();
            }
        }
        if (checkKey(mapTest, "ignoreSystemLifecycleBootstrap")) {
            boolean value = mapTest.getBoolean("ignoreSystemLifecycleBootstrap");
            testOptions.ignoreSystemLifecycleBootstrap = value;
        }

        com.adjust.sdk.Adjust.setTestOptions(testOptions);
    }

    @ReactMethod
    public void teardown() {
        this.isAttributionCallbackImplemented = false;
        this.isEventTrackingSucceededCallbackImplemented = false;
        this.isEventTrackingFailedCallbackImplemented = false;
        this.isSessionTrackingSucceededCallbackImplemented = false;
        this.isSessionTrackingFailedCallbackImplemented = false;
        this.isDeferredDeeplinkCallbackImplemented = false;
    }

    // private & helper methods

    private void sendEvent(
        final ReactContext reactContext,
        final String eventName,
        final @Nullable WritableMap params) {
        reactContext
            .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
            .emit(eventName, params);
    }

    private boolean checkKey(final ReadableMap map, final String key) {
        return map.hasKey(key) && !map.isNull(key);
    }

    @ReactMethod
    public void addListener(String eventName) {
        // keep: required for RN built in event emitter calls
    }

    @ReactMethod
    public void removeListeners(double count) {
        // keep: required for RN built in event emitter calls
    }
}
