<commit_before>package io.customerly;

/*
 * Copyright (C) 2017 Customerly
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.CheckResult;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.SpannableStringBuilder;
import android.util.Log;
import android.util.Patterns;
import android.view.WindowManager;

import org.intellij.lang.annotations.Subst;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Locale;

import io.socket.client.IO;
import io.socket.client.Socket;

/**
 * The singleton representing the Customerly SDK
 */
@SuppressWarnings({"WeakerAccess", "unused"})
public class Customerly {

    private static final String PREFS_PING_RESPONSE__WIDGET_COLOR = "PREFS_PING_RESPONSE__WIDGET_COLOR";
    private static final String PREFS_PING_RESPONSE__BACKGROUND_THEME_URL = "PREFS_PING_RESPONSE__BACKGROUND_THEME_URL";
    private static final String PREFS_PING_RESPONSE__POWERED_BY = "PREFS_PING_RESPONSE__POWERED_BY";
    private static final String PREFS_PING_RESPONSE__WELCOME_USERS = "PREFS_PING_RESPONSE__WELCOME_USERS";
    private static final String PREFS_PING_RESPONSE__WELCOME_VISITORS = "PREFS_PING_RESPONSE__WELCOME_VISITORS";
    private static final String PREF_CURRENT_EMAIL = "regusrml", PREF_CURRENT_ID = "regusrid", PREF_CURRENT_COMPANY_INFO = "cmpnynfo";
    private static final long SOCKET_PING_INTERVAL = 60000, SURVEY_DISPLAY_DELAY = 5000L;
    private static final String SOCKET_EVENT__PING = "p";
    private static final String SOCKET_EVENT__PING_ACTIVE = "a";
    private static final String SOCKET_EVENT__TYPING = "typing";
    private static final String SOCKET_EVENT__SEEN = "seen";
    private static final String SOCKET_EVENT__MESSAGE = "message";
    @ColorInt private static final int DEF_WIDGET_COLOR_MALIBU_INT = 0xff65a9e7;//Blue Malibu

    @NonNull final IU_RemoteImageHandler _RemoteImageHandler = new IU_RemoteImageHandler();
    @NonNull private final Handler __Handler = new Handler();

    private boolean initialized = false;
    private @Nullable SharedPreferences _SharedPreferences;
    @Nullable String _AppID, _AppCacheDir;
    @ColorInt private int
            __WidgetColor__Fallback = DEF_WIDGET_COLOR_MALIBU_INT,
            __WidgetColor__HardCoded = Color.TRANSPARENT;

    @Nullable IE_JwtToken _JwtToken;

    private boolean __VerboseLogging = false;

    @Nullable private Socket _Socket;

    private long __PING__next_ping_allowed = 0L;
    @ColorInt int __PING__LAST_widget_color;
    @Nullable String __PING__LAST_widget_background_url;
    boolean __PING__LAST_powered_by;
    @Nullable private String __PING__LAST_welcome_message_users, __PING__LAST_welcome_message_visitors;
    @Nullable IE_Admin[] __PING__LAST_active_admins;

    @NonNull JSONObject __PING__DeviceJSON = new JSONObject();

    @Nullable private WeakReference<Activity> _CurrentActivity;
    @NonNull private final Application.ActivityLifecycleCallbacks __ActivityLifecycleCallbacks = new Application.ActivityLifecycleCallbacks() {
        private boolean paused = false, foreground = false;
        @NonNull private Runnable checkBackground = () -> {
            if (this.foreground && this.paused) {
                this.foreground = false;
                __SOCKET__disconnect(_Socket);
            }
        };
        @Override public void onActivityResumed(Activity activity) {
            _CurrentActivity = new WeakReference<>(activity);
            this.paused = false;
            boolean wasBackground = !this.foreground;
            this.foreground = true;
            __Handler.removeCallbacks(this.checkBackground);
            if (wasBackground) {
                __SOCKET__connect(null);
                __PING__Start(null, null);
            }
        }
        @Override public void onActivityPaused(Activity activity) {
            this.paused = true;
            __Handler.removeCallbacks(this.checkBackground);
            __Handler.postDelayed(this.checkBackground, 500);
        }
        @Override public void onActivityStopped(Activity activity) { }
        @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { }
        @Override public void onActivityDestroyed(Activity activity) {
            PW_AlertMessage.onActivityDestroyed(activity);//Need to dismiss the alert or leak window exception comes out
        }
        @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { }
        @Override public void onActivityStarted(Activity activity) { }
    };

    private boolean _SupportEnabled = true, _SurveyEnabled = true;
    private boolean __PING__checkConnectedToSocket = false;

    private class PingResponseConverter implements IApi_Request.ResponseConverter<Void> {
        private final boolean _HandleAlertMessage, _HandleSurvey;
        private PingResponseConverter(boolean handleSurvey, boolean handleAlertMessage) {
            super();
            this._HandleSurvey = handleSurvey;
            this._HandleAlertMessage = handleAlertMessage;
        }
        @Nullable
        @Override
        public final Void convert(@NonNull JSONObject root) throws JSONException {
            __PING__next_ping_allowed = root.optLong("next-ping-allowed", 0);
            __SOCKET__connect(root.optJSONObject("websocket"));
            JSONObject app_config = root.optJSONObject("app_config");
            if(app_config != null) {
                if(__WidgetColor__HardCoded == Color.TRANSPARENT) {
                    String pingWidgetColor = IU_Utils.jsonOptStringWithNullCheck(app_config, "widget_color");
                    if (pingWidgetColor != null && pingWidgetColor.length() != 0) {
                        if (pingWidgetColor.charAt(0) != '#') {
                            pingWidgetColor = '#' + pingWidgetColor;
                        }
                        try {
                            __PING__LAST_widget_color = Color.parseColor(pingWidgetColor);
                        } catch (IllegalArgumentException notCorrectColor) {
                            IEr_CustomerlyErrorHandler.sendError(IEr_CustomerlyErrorHandler.ERROR_CODE__HTTP_RESPONSE_ERROR, String.format("PingResponse:data.apps.app_config.widget_color is an invalid argb color: '%s'", pingWidgetColor), notCorrectColor);
                            __PING__LAST_widget_color = __WidgetColor__Fallback;
                        }
                    }
                }
                __PING__LAST_widget_background_url = IU_Utils.jsonOptStringWithNullCheck(app_config, "widget_background_url");
                __PING__LAST_powered_by = 1 == app_config.optLong("powered_by", 0);
                __PING__LAST_welcome_message_users = IU_Utils.jsonOptStringWithNullCheck(app_config, "welcome_message_users");
                __PING__LAST_welcome_message_visitors = IU_Utils.jsonOptStringWithNullCheck(app_config, "welcome_message_visitors");
            } else {
                __PING__LAST_widget_color = __WidgetColor__Fallback;
                __PING__LAST_widget_background_url = null;
                __PING__LAST_powered_by = false;
                __PING__LAST_welcome_message_users = null;
                __PING__LAST_welcome_message_visitors = null;
            }
            __PING__LAST_active_admins = IE_Admin.from(root.optJSONArray("active_admins"));

            final SharedPreferences prefs = _SharedPreferences;
            if(prefs != null) {
                prefs.edit()
                        .putInt(PREFS_PING_RESPONSE__WIDGET_COLOR, __PING__LAST_widget_color)
                        .putString(PREFS_PING_RESPONSE__BACKGROUND_THEME_URL, __PING__LAST_widget_background_url)
                        .putBoolean(PREFS_PING_RESPONSE__POWERED_BY, __PING__LAST_powered_by)
                        .putString(PREFS_PING_RESPONSE__WELCOME_USERS, __PING__LAST_welcome_message_users)
                        .putString(PREFS_PING_RESPONSE__WELCOME_VISITORS, __PING__LAST_welcome_message_visitors)
                        .apply();
            }

            if(this._HandleSurvey && _SurveyEnabled) {
                _log("Attempting to display a Survey");
                IE_Survey[] surveys = IE_Survey.from(root.optJSONArray("last_surveys"));
                if(surveys != null) {
                    for (final IE_Survey survey : surveys) {
                        if (survey != null && !survey.isRejectedOrConcluded) {
                            __Handler.postDelayed(() -> {
                                Activity activity = _CurrentActivity == null ? null : _CurrentActivity.get();
                                if(activity != null) {
                                    try {
                                        try {
                                            IDlgF_Survey.show(activity, survey);
                                        } catch (WindowManager.BadTokenException changedActivityWhileRunning) {
                                            activity = _CurrentActivity == null ? null : _CurrentActivity.get();
                                            if (activity != null) {
                                                try {
                                                    IDlgF_Survey.show(activity, survey);
                                                    _log("Survey successfully displayed");
                                                } catch (WindowManager.BadTokenException ignored) {
                                                    //Second failure.
                                                }
                                            }
                                        }
                                    } catch (Exception generic) {
                                        _log("A generic error occurred in Customerly.openSurvey");
                                        IEr_CustomerlyErrorHandler.sendError(IEr_CustomerlyErrorHandler.ERROR_CODE__GENERIC, "Generic error in Customerly.openSurvey", generic);
                                    }
                                }
                            }, SURVEY_DISPLAY_DELAY);
                            return null;
                        }
                    }
                }
                _log("No Survey to display");
            }

            if(this._HandleAlertMessage && _SupportEnabled) {
                _log("Attempting to display an unread message");
                JSONArray last_messages_array = root.optJSONArray("last_messages");
                if (last_messages_array != null && last_messages_array.length() != 0) {
                    for (int i = 0; i < last_messages_array.length(); i++) {
                        try {
                            final JSONObject message = last_messages_array.getJSONObject(i);
                            if (message != null) {
                                __Handler.post(() -> {
                                    Activity activity = _CurrentActivity == null ? null : _CurrentActivity.get();
                                    if (activity != null) {
                                        if (activity instanceof SDKActivity) {
                                            ArrayList<IE_Message> list = new ArrayList<>(1);
                                            list.add(new IE_Message(message));
                                            ((SDKActivity) activity).onNewSocketMessages(list);
                                        } else {
                                            try {
                                                PW_AlertMessage.show(activity, new IE_Message(message));
                                                _log("Message alert displayed successfully");
                                            } catch (WindowManager.BadTokenException changedActivityWhileExecuting) {
                                                activity = _CurrentActivity == null ? null : _CurrentActivity.get();
                                                if (activity != null) {
                                                    if (activity instanceof SDKActivity) {
                                                        ArrayList<IE_Message> list = new ArrayList<>(1);
                                                        list.add(new IE_Message(message));
                                                        ((SDKActivity) activity).onNewSocketMessages(list);
                                                    } else {
                                                        try {
                                                            PW_AlertMessage.show(activity, new IE_Message(message));
                                                            _log("Message alert displayed successfully");
                                                        } catch (WindowManager.BadTokenException ignored) {
                                                            _log("An error occours while attaching the alertmessage to the window. Activity: " + activity.toString());
                                                            //Second try failure.
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                });
                                return null;
                            }
                        } catch (JSONException ignored) {
                        }
                    }
                }
                _log("no messages to display");
            }
            return null;
        }
    }

    @NonNull private final PingResponseConverter __PING__response_converter__SurveyMessage = new PingResponseConverter(true, true),
            __PING__response_converter__Message = new PingResponseConverter(false, true),
            __PING__response_converter__NaN = new PingResponseConverter(false, false);

    @NonNull private static final Customerly _Instance = new Customerly();

    private Customerly() {
        super();
        try {
            this.__PING__DeviceJSON.put("os", "Android")
                    .put("device", String.format("%s %s (%s)", Build.MANUFACTURER, Build.MODEL, Build.DEVICE))
                    .put("os_version", Build.VERSION.SDK_INT)
                    .put("sdk_version", BuildConfig.VERSION_CODE)
                    .put("api_version", BuildConfig.CUSTOMERLY_API_VERSION)
                    .put("socket_version", BuildConfig.CUSTOMERLY_SOCKET_VERSION);
        } catch (JSONException ignored) { }
    }

    boolean _isConfigured() {
        if(this._AppID == null) {
            this._log("You need to configure the SDK ");
            IEr_CustomerlyErrorHandler.sendNotConfiguredError();
            return false;
        } else {
            return true;
        }
    }

    void _log(@NonNull String pLogMessage) {
        if(this.__VerboseLogging) {
            Log.v(BuildConfig.CUSTOMERLY_SDK_NAME, pLogMessage);
        }
    }

    private void _DEV_log(@NonNull String message) {
        if(BuildConfig.CUSTOMERLY_DEV_MODE) {
            Log.e(BuildConfig.CUSTOMERLY_SDK_NAME, message);
        }
    }

    @Nullable HtmlMessage _WELCOME__getMessage() {
        IE_JwtToken token = this._JwtToken;
        return this._isConfigured()
                ? IU_Utils.decodeHtmlStringWithEmojiTag(token != null && token.isUser() ? this.__PING__LAST_welcome_message_users : this.__PING__LAST_welcome_message_visitors)
                : null;
    }

    interface __SOCKET__ITyping_listener {   void onTypingEvent(long pConversationID, long account_id, boolean pTyping);   }
    @Nullable __SOCKET__ITyping_listener __SOCKET__Typing_listener = null;
    @Nullable private String __SOCKET__Endpoint = null, __SOCKET__Port = null;
    @Nullable private String __SOCKET__CurrentConfiguration = null;
    @NonNull private final Runnable __SOCKET__ping = () -> {
        Socket socket = this._Socket;
        if(this.__PING__checkConnectedToSocket) {
            if (socket != null) {
                Activity activity = _CurrentActivity == null ? null : _CurrentActivity.get();
                socket.emit(activity != null && activity instanceof SDKActivity ? SOCKET_EVENT__PING_ACTIVE : SOCKET_EVENT__PING);
                this.__Handler.postDelayed(this.__SOCKET__ping, SOCKET_PING_INTERVAL);
            }
            if(socket == null || !socket.connected()) {
                this.__SOCKET__connect(null);
            }
        }
    };
    interface SDKActivity {
        void onNewSocketMessages(@NonNull ArrayList<IE_Message> messages);
        void onLogoutUser();
    }
    private void __SOCKET__connect(@Nullable JSONObject webSocket) {
        if (webSocket != null) {
        /*  "webSocket": {
              "endpoint": "https://ws2.customerly.io",
              "port": "8080"  }  */
            this.__SOCKET__Endpoint = IU_Utils.jsonOptStringWithNullCheck(webSocket, "endpoint");
            this.__SOCKET__Port = IU_Utils.jsonOptStringWithNullCheck(webSocket, "port");
        }

        if(this._SupportEnabled) {
            this.__PING__checkConnectedToSocket = true;//Metto a true perchè potremmo non entrare nei successivi if
            if (this._AppID != null && this.__SOCKET__Endpoint != null && this.__SOCKET__Port != null) {
                IE_JwtToken token = this._JwtToken;
                if (token != null && token._UserID != null) {
                    Socket socket = this._Socket;
                    if (socket == null || ! socket.connected() || this.__SOCKET__CurrentConfiguration == null || !this.__SOCKET__CurrentConfiguration.equals(String.format(Locale.UK, "%s-%s-%d", this.__SOCKET__Endpoint, this.__SOCKET__Port, token._UserID))) {

                        this.__SOCKET__disconnect(socket);
                        this.__PING__checkConnectedToSocket = true;//Rimetto a true perchè la disconnect l'ha messo a false
                        this.__SOCKET__CurrentConfiguration = String.format(Locale.UK, "%s-%s-%d", this.__SOCKET__Endpoint, this.__SOCKET__Port, token._UserID);

                        String query;
                        try {
                            query = "json=" + new JSONObject().put("nsp", "user").put("is_mobile", true).put("app", this._AppID).put("id", token._UserID).put("socket_version", BuildConfig.CUSTOMERLY_SOCKET_VERSION).toString();
                        } catch (JSONException error) {
                            return;
                        }
                        try {
                            IO.Options options = new IO.Options();
                            options.secure = true;
                            options.forceNew = true;
                            options.reconnectionDelay = 15000;
                            options.reconnectionDelayMax = 60000;
                            options.query = query;

                            socket = IO.socket(String.format("%s:%s/", this.__SOCKET__Endpoint, this.__SOCKET__Port), options);
                            if(socket != null) {
                                this._Socket = socket;
                                socket.on(SOCKET_EVENT__TYPING, payload -> {
                                    if (payload.length != 0) {
                                        try {
                                            JSONObject payloadJson = (JSONObject) payload[0];
                                    /*  {   client : {account_id: 82, name: "Gianni"},
                                            conversation : {conversation_id: "173922", account_id: 82, user_id: 55722, is_note: false},
                                            is_typing : "n" } */
                                            JSONObject client = payloadJson.optJSONObject("client");
                                            if (client != null) {
                                                long account_id = client.optLong("account_id", -1L);
                                                if (account_id != -1L) {
                                                    try {
                                                        this._DEV_log(String.format("SOCKET RX: %1$s -> %2$s", SOCKET_EVENT__TYPING, payloadJson.toString(1)));
                                                    } catch (JSONException ignored) {
                                                    }
                                                    boolean is_typing = "y".equals(payloadJson.optString("is_typing"));
                                                    payloadJson = payloadJson.getJSONObject("conversation");
                                                    if (payloadJson != null) {
                                                        IE_JwtToken token2 = this._JwtToken;
                                                        if (token2 != null && token2._UserID != null && token2._UserID == payloadJson.getLong("user_id") && !payloadJson.optBoolean("is_note", false)) {
                                                            long conversation_id = payloadJson.optLong("conversation_id", 0);
                                                            __SOCKET__ITyping_listener listener = this.__SOCKET__Typing_listener;
                                                            if (conversation_id != 0 && listener != null) {
                                                                listener.onTypingEvent(conversation_id, account_id, is_typing);
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                        } catch (JSONException ignored) {
                                        }
                                    }
                                });
                                socket.on(SOCKET_EVENT__MESSAGE, payload -> {
                                    if (payload.length != 0) {
                                        try {
                                            JSONObject payloadJson = (JSONObject) payload[0];
                                    /*
                                    {user_id: 41897, account_id: 82, timestamp: 1483388854, from_account: true,
                                        conversation : {is_note: false} }
                                     */
                                            if (payloadJson.optBoolean("from_account")) {
                                                try {
                                                    this._log("Message received via socket");
                                                    this._DEV_log(String.format("SOCKET RX: %1$s -> %2$s", SOCKET_EVENT__MESSAGE, payloadJson.toString(1)));
                                                } catch (JSONException ignored) {
                                                }
                                                long timestamp = payloadJson.optLong("timestamp", 0L);
                                                long socket_user_id = payloadJson.optLong("user_id", 0L);

                                                IE_JwtToken token2 = this._JwtToken;
                                                if (token2 != null && token2._UserID != null && token2._UserID == socket_user_id
                                                        && socket_user_id != 0 && timestamp != 0
                                                        && !payloadJson.getJSONObject("conversation").optBoolean("is_note", false)) {
                                                    new IApi_Request.Builder<ArrayList<IE_Message>>(IApi_Request.ENDPOINT_MESSAGE_NEWS)
                                                            .opt_converter(data -> IU_Utils.fromJSONdataToList(data, "messages", IE_Message::new))
                                                            .opt_tokenMandatory()
                                                            .opt_receiver((responseState, new_messages) -> {
                                                                if (responseState == IApi_Request.RESPONSE_STATE__OK && new_messages != null && new_messages.size() != 0) {
                                                                    Activity activity = _CurrentActivity == null ? null : _CurrentActivity.get();
                                                                    if (activity != null) {
                                                                        if (activity instanceof SDKActivity) {
                                                                            ((SDKActivity) activity).onNewSocketMessages(new_messages);
                                                                        } else if (this._SupportEnabled) {
                                                                            try {
                                                                                PW_AlertMessage.show(activity, new_messages.get(0));
                                                                            } catch (WindowManager.BadTokenException changedActivityWhileExecuting) {
                                                                                activity = _CurrentActivity == null ? null : _CurrentActivity.get();
                                                                                if (activity != null) {
                                                                                    if (activity instanceof SDKActivity) {
                                                                                        ((SDKActivity) activity).onNewSocketMessages(new_messages);
                                                                                    } else {
                                                                                        try {
                                                                                            PW_AlertMessage.show(activity, new_messages.get(0));
                                                                                        } catch (WindowManager.BadTokenException ignored) {
                                                                                            //Second try failure.
                                                                                        }
                                                                                    }
                                                                                }
                                                                            }
                                                                        }
                                                                    }
                                                                }
                                                            })
                                                            .param("timestamp", timestamp)
                                                            .start();
                                                }
                                            }
                                        } catch (JSONException ignored) {
                                        }
                                    }
                                });

                                socket.connect();
                                this.__Handler.postDelayed(this.__SOCKET__ping, SOCKET_PING_INTERVAL);
                            }
                        } catch (Exception ignored) { }
                    }
                }
            }
        }
    }
    private void __SOCKET__disconnect(@Nullable Socket socket) {
        this.__PING__checkConnectedToSocket = false;
        if (socket != null) {
            if (socket.connected()) {
                socket.disconnect();
            }
            this._Socket = null;
            this.__SOCKET__CurrentConfiguration = null;
        }
        this.__Handler.removeCallbacks(this.__SOCKET__ping);
    }
    private void __SOCKET__SEND(@NonNull String event, @NonNull JSONObject payloadJson) {
        Socket socket = this._Socket;
        if(this.__PING__checkConnectedToSocket) {
            if(socket != null) {
                try {
                    this._DEV_log(String.format("SOCKET TX: %1$s -> %2$s", event, payloadJson.toString(1)));
                } catch (JSONException ignored) {
                }
                socket.emit(event, payloadJson);
            }
            if(socket == null || !socket.connected()) {
                this.__SOCKET__connect(null);
            }
        }
    }

    void __SOCKET_SEND_Typing(long pConversationID, boolean pTyping, @Nullable String pText) {
        //{conversation: {conversation_id: 179170, user_id: 63378, is_note: false}, is_typing: "y", typing_preview: "I am writ"}
        IE_JwtToken token = this._JwtToken;
        if(token != null && token._UserID != null) {
            try {
                this.__SOCKET__SEND(SOCKET_EVENT__TYPING, new JSONObject()
                        .put("conversation", new JSONObject()
                                .put("conversation_id", pConversationID)
                                .put("user_id", token._UserID)
                                .put("is_note", false))
                        .put("is_typing", pTyping ? "y" : "n")
                        .put("typing_preview", pText));
            } catch (JSONException ignored) { }
        }
    }
    void __SOCKET_SEND_Message(long pTimestamp) {
        if(pTimestamp != -1L) {
            IE_JwtToken token = this._JwtToken;
            if (token != null && token._UserID != null) {<commit_msg>[BETA-3.1.4] Migliore gestione disconnessione socket<commit_after>package io.customerly;

/*
 * Copyright (C) 2017 Customerly
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.CheckResult;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.SpannableStringBuilder;
import android.util.Log;
import android.util.Patterns;
import android.view.WindowManager;

import org.intellij.lang.annotations.Subst;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Locale;

import io.socket.client.IO;
import io.socket.client.Socket;

/**
 * The singleton representing the Customerly SDK
 */
@SuppressWarnings({"WeakerAccess", "unused"})
public class Customerly {

    private static final String PREFS_PING_RESPONSE__WIDGET_COLOR = "PREFS_PING_RESPONSE__WIDGET_COLOR";
    private static final String PREFS_PING_RESPONSE__BACKGROUND_THEME_URL = "PREFS_PING_RESPONSE__BACKGROUND_THEME_URL";
    private static final String PREFS_PING_RESPONSE__POWERED_BY = "PREFS_PING_RESPONSE__POWERED_BY";
    private static final String PREFS_PING_RESPONSE__WELCOME_USERS = "PREFS_PING_RESPONSE__WELCOME_USERS";
    private static final String PREFS_PING_RESPONSE__WELCOME_VISITORS = "PREFS_PING_RESPONSE__WELCOME_VISITORS";
    private static final String PREF_CURRENT_EMAIL = "regusrml", PREF_CURRENT_ID = "regusrid", PREF_CURRENT_COMPANY_INFO = "cmpnynfo";
    private static final long SOCKET_PING_INTERVAL = 60000, SURVEY_DISPLAY_DELAY = 5000L;
    private static final String SOCKET_EVENT__PING = "p";
    private static final String SOCKET_EVENT__PING_ACTIVE = "a";
    private static final String SOCKET_EVENT__TYPING = "typing";
    private static final String SOCKET_EVENT__SEEN = "seen";
    private static final String SOCKET_EVENT__MESSAGE = "message";
    @ColorInt private static final int DEF_WIDGET_COLOR_MALIBU_INT = 0xff65a9e7;//Blue Malibu

    @NonNull final IU_RemoteImageHandler _RemoteImageHandler = new IU_RemoteImageHandler();
    @NonNull private final Handler __Handler = new Handler();

    private boolean initialized = false;
    private @Nullable SharedPreferences _SharedPreferences;
    @Nullable String _AppID, _AppCacheDir;
    @ColorInt private int
            __WidgetColor__Fallback = DEF_WIDGET_COLOR_MALIBU_INT,
            __WidgetColor__HardCoded = Color.TRANSPARENT;

    @Nullable IE_JwtToken _JwtToken;

    private boolean __VerboseLogging = false;

    @Nullable private Socket _Socket;

    private long __PING__next_ping_allowed = 0L;
    @ColorInt int __PING__LAST_widget_color;
    @Nullable String __PING__LAST_widget_background_url;
    boolean __PING__LAST_powered_by;
    @Nullable private String __PING__LAST_welcome_message_users, __PING__LAST_welcome_message_visitors;
    @Nullable IE_Admin[] __PING__LAST_active_admins;

    @NonNull JSONObject __PING__DeviceJSON = new JSONObject();

    @Nullable private WeakReference<Activity> _CurrentActivity;
    @NonNull private final Application.ActivityLifecycleCallbacks __ActivityLifecycleCallbacks = new Application.ActivityLifecycleCallbacks() {
        private boolean paused = false, foreground = false;
        @NonNull private Runnable checkBackground = () -> {
            if (this.foreground && this.paused) {
                this.foreground = false;
                __SOCKET__disconnect(_Socket);
            }
        };
        @Override public void onActivityResumed(Activity activity) {
            _CurrentActivity = new WeakReference<>(activity);
            this.paused = false;
            boolean wasBackground = !this.foreground;
            this.foreground = true;
            __Handler.removeCallbacks(this.checkBackground);
            if (wasBackground) {
                __SOCKET__connect(null);
                __PING__Start(null, null);
            }
        }
        @Override public void onActivityPaused(Activity activity) {
            this.paused = true;
            __Handler.removeCallbacks(this.checkBackground);
            __Handler.postDelayed(this.checkBackground, 500);
        }
        @Override public void onActivityStopped(Activity activity) { }
        @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { }
        @Override public void onActivityDestroyed(Activity activity) {
            PW_AlertMessage.onActivityDestroyed(activity);//Need to dismiss the alert or leak window exception comes out
        }
        @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { }
        @Override public void onActivityStarted(Activity activity) { }
    };

    private boolean _SupportEnabled = true, _SurveyEnabled = true;

    private class PingResponseConverter implements IApi_Request.ResponseConverter<Void> {
        private final boolean _HandleAlertMessage, _HandleSurvey;
        private PingResponseConverter(boolean handleSurvey, boolean handleAlertMessage) {
            super();
            this._HandleSurvey = handleSurvey;
            this._HandleAlertMessage = handleAlertMessage;
        }
        @Nullable
        @Override
        public final Void convert(@NonNull JSONObject root) throws JSONException {
            __PING__next_ping_allowed = root.optLong("next-ping-allowed", 0);
            __SOCKET__connect(root.optJSONObject("websocket"));
            JSONObject app_config = root.optJSONObject("app_config");
            if(app_config != null) {
                if(__WidgetColor__HardCoded == Color.TRANSPARENT) {
                    String pingWidgetColor = IU_Utils.jsonOptStringWithNullCheck(app_config, "widget_color");
                    if (pingWidgetColor != null && pingWidgetColor.length() != 0) {
                        if (pingWidgetColor.charAt(0) != '#') {
                            pingWidgetColor = '#' + pingWidgetColor;
                        }
                        try {
                            __PING__LAST_widget_color = Color.parseColor(pingWidgetColor);
                        } catch (IllegalArgumentException notCorrectColor) {
                            IEr_CustomerlyErrorHandler.sendError(IEr_CustomerlyErrorHandler.ERROR_CODE__HTTP_RESPONSE_ERROR, String.format("PingResponse:data.apps.app_config.widget_color is an invalid argb color: '%s'", pingWidgetColor), notCorrectColor);
                            __PING__LAST_widget_color = __WidgetColor__Fallback;
                        }
                    }
                }
                __PING__LAST_widget_background_url = IU_Utils.jsonOptStringWithNullCheck(app_config, "widget_background_url");
                __PING__LAST_powered_by = 1 == app_config.optLong("powered_by", 0);
                __PING__LAST_welcome_message_users = IU_Utils.jsonOptStringWithNullCheck(app_config, "welcome_message_users");
                __PING__LAST_welcome_message_visitors = IU_Utils.jsonOptStringWithNullCheck(app_config, "welcome_message_visitors");
            } else {
                __PING__LAST_widget_color = __WidgetColor__Fallback;
                __PING__LAST_widget_background_url = null;
                __PING__LAST_powered_by = false;
                __PING__LAST_welcome_message_users = null;
                __PING__LAST_welcome_message_visitors = null;
            }
            __PING__LAST_active_admins = IE_Admin.from(root.optJSONArray("active_admins"));

            final SharedPreferences prefs = _SharedPreferences;
            if(prefs != null) {
                prefs.edit()
                        .putInt(PREFS_PING_RESPONSE__WIDGET_COLOR, __PING__LAST_widget_color)
                        .putString(PREFS_PING_RESPONSE__BACKGROUND_THEME_URL, __PING__LAST_widget_background_url)
                        .putBoolean(PREFS_PING_RESPONSE__POWERED_BY, __PING__LAST_powered_by)
                        .putString(PREFS_PING_RESPONSE__WELCOME_USERS, __PING__LAST_welcome_message_users)
                        .putString(PREFS_PING_RESPONSE__WELCOME_VISITORS, __PING__LAST_welcome_message_visitors)
                        .apply();
            }

            if(this._HandleSurvey && _SurveyEnabled) {
                _log("Attempting to display a Survey");
                IE_Survey[] surveys = IE_Survey.from(root.optJSONArray("last_surveys"));
                if(surveys != null) {
                    for (final IE_Survey survey : surveys) {
                        if (survey != null && !survey.isRejectedOrConcluded) {
                            __Handler.postDelayed(() -> {
                                Activity activity = _CurrentActivity == null ? null : _CurrentActivity.get();
                                if(activity != null) {
                                    try {
                                        try {
                                            IDlgF_Survey.show(activity, survey);
                                        } catch (WindowManager.BadTokenException changedActivityWhileRunning) {
                                            activity = _CurrentActivity == null ? null : _CurrentActivity.get();
                                            if (activity != null) {
                                                try {
                                                    IDlgF_Survey.show(activity, survey);
                                                    _log("Survey successfully displayed");
                                                } catch (WindowManager.BadTokenException ignored) {
                                                    //Second failure.
                                                }
                                            }
                                        }
                                    } catch (Exception generic) {
                                        _log("A generic error occurred in Customerly.openSurvey");
                                        IEr_CustomerlyErrorHandler.sendError(IEr_CustomerlyErrorHandler.ERROR_CODE__GENERIC, "Generic error in Customerly.openSurvey", generic);
                                    }
                                }
                            }, SURVEY_DISPLAY_DELAY);
                            return null;
                        }
                    }
                }
                _log("No Survey to display");
            }

            if(this._HandleAlertMessage && _SupportEnabled) {
                _log("Attempting to display an unread message");
                JSONArray last_messages_array = root.optJSONArray("last_messages");
                if (last_messages_array != null && last_messages_array.length() != 0) {
                    for (int i = 0; i < last_messages_array.length(); i++) {
                        try {
                            final JSONObject message = last_messages_array.getJSONObject(i);
                            if (message != null) {
                                __Handler.post(() -> {
                                    Activity activity = _CurrentActivity == null ? null : _CurrentActivity.get();
                                    if (activity != null) {
                                        if (activity instanceof SDKActivity) {
                                            ArrayList<IE_Message> list = new ArrayList<>(1);
                                            list.add(new IE_Message(message));
                                            ((SDKActivity) activity).onNewSocketMessages(list);
                                        } else {
                                            try {
                                                PW_AlertMessage.show(activity, new IE_Message(message));
                                                _log("Message alert displayed successfully");
                                            } catch (WindowManager.BadTokenException changedActivityWhileExecuting) {
                                                activity = _CurrentActivity == null ? null : _CurrentActivity.get();
                                                if (activity != null) {
                                                    if (activity instanceof SDKActivity) {
                                                        ArrayList<IE_Message> list = new ArrayList<>(1);
                                                        list.add(new IE_Message(message));
                                                        ((SDKActivity) activity).onNewSocketMessages(list);
                                                    } else {
                                                        try {
                                                            PW_AlertMessage.show(activity, new IE_Message(message));
                                                            _log("Message alert displayed successfully");
                                                        } catch (WindowManager.BadTokenException ignored) {
                                                            _log("An error occours while attaching the alertmessage to the window. Activity: " + activity.toString());
                                                            //Second try failure.
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                });
                                return null;
                            }
                        } catch (JSONException ignored) {
                        }
                    }
                }
                _log("no messages to display");
            }
            return null;
        }
    }

    @NonNull private final PingResponseConverter __PING__response_converter__SurveyMessage = new PingResponseConverter(true, true),
            __PING__response_converter__Message = new PingResponseConverter(false, true),
            __PING__response_converter__NaN = new PingResponseConverter(false, false);

    @NonNull private static final Customerly _Instance = new Customerly();

    private Customerly() {
        super();
        try {
            this.__PING__DeviceJSON.put("os", "Android")
                    .put("device", String.format("%s %s (%s)", Build.MANUFACTURER, Build.MODEL, Build.DEVICE))
                    .put("os_version", Build.VERSION.SDK_INT)
                    .put("sdk_version", BuildConfig.VERSION_CODE)
                    .put("api_version", BuildConfig.CUSTOMERLY_API_VERSION)
                    .put("socket_version", BuildConfig.CUSTOMERLY_SOCKET_VERSION);
        } catch (JSONException ignored) { }
    }

    boolean _isConfigured() {
        if(this._AppID == null) {
            this._log("You need to configure the SDK ");
            IEr_CustomerlyErrorHandler.sendNotConfiguredError();
            return false;
        } else {
            return true;
        }
    }

    void _log(@NonNull String pLogMessage) {
        if(this.__VerboseLogging) {
            Log.v(BuildConfig.CUSTOMERLY_SDK_NAME, pLogMessage);
        }
    }

    private void _DEV_log(@NonNull String message) {
        if(BuildConfig.CUSTOMERLY_DEV_MODE) {
            Log.e(BuildConfig.CUSTOMERLY_SDK_NAME, message);
        }
    }

    @Nullable HtmlMessage _WELCOME__getMessage() {
        IE_JwtToken token = this._JwtToken;
        return this._isConfigured()
                ? IU_Utils.decodeHtmlStringWithEmojiTag(token != null && token.isUser() ? this.__PING__LAST_welcome_message_users : this.__PING__LAST_welcome_message_visitors)
                : null;
    }

    interface __SOCKET__ITyping_listener {   void onTypingEvent(long pConversationID, long account_id, boolean pTyping);   }
    @Nullable __SOCKET__ITyping_listener __SOCKET__Typing_listener = null;
    @Nullable private String __SOCKET__Endpoint = null, __SOCKET__Port = null;
    @Nullable private String __SOCKET__CurrentConfiguration = null;
    private boolean __SOCKET__shouldBeConnected = false;
    @NonNull private final Runnable __SOCKET__ping = () -> {
        this.__SOCKET__check();
        Socket socket = this._Socket;
        if (socket != null) {
            Activity activity = _CurrentActivity == null ? null : _CurrentActivity.get();
            socket.emit(activity != null && activity instanceof SDKActivity ? SOCKET_EVENT__PING_ACTIVE : SOCKET_EVENT__PING);
            this.__Handler.postDelayed(this.__SOCKET__ping, SOCKET_PING_INTERVAL);
        }
    };
    interface SDKActivity {
        void onNewSocketMessages(@NonNull ArrayList<IE_Message> messages);
        void onLogoutUser();
    }
    private void __SOCKET__connect(@Nullable JSONObject webSocket) {
        if (webSocket != null) {
        /*  "webSocket": {
              "endpoint": "https://ws2.customerly.io",
              "port": "8080"  }  */
            this.__SOCKET__Endpoint = IU_Utils.jsonOptStringWithNullCheck(webSocket, "endpoint");
            this.__SOCKET__Port = IU_Utils.jsonOptStringWithNullCheck(webSocket, "port");
        }

        if(this._SupportEnabled) {
            this.__SOCKET__shouldBeConnected = true;//Metto a true perchè potremmo non entrare nei successivi if
            if (this._AppID != null && this.__SOCKET__Endpoint != null && this.__SOCKET__Port != null) {
                IE_JwtToken token = this._JwtToken;
                if (token != null && token._UserID != null) {
                    Socket socket = this._Socket;
                    if (socket == null || ! socket.connected() || this.__SOCKET__CurrentConfiguration == null || !this.__SOCKET__CurrentConfiguration.equals(String.format(Locale.UK, "%s-%s-%d", this.__SOCKET__Endpoint, this.__SOCKET__Port, token._UserID))) {

                        this.__SOCKET__disconnect(socket);
                        this.__SOCKET__shouldBeConnected = true;//Rimetto a true perchè la disconnect l'ha messo a false
                        this.__SOCKET__CurrentConfiguration = String.format(Locale.UK, "%s-%s-%d", this.__SOCKET__Endpoint, this.__SOCKET__Port, token._UserID);

                        String query;
                        try {
                            query = "json=" + new JSONObject().put("nsp", "user").put("is_mobile", true).put("app", this._AppID).put("id", token._UserID).put("socket_version", BuildConfig.CUSTOMERLY_SOCKET_VERSION).toString();
                        } catch (JSONException error) {
                            return;
                        }
                        try {
                            IO.Options options = new IO.Options();
                            options.secure = true;
                            options.forceNew = true;
                            options.reconnectionDelay = 15000;
                            options.reconnectionDelayMax = 60000;
                            options.query = query;

                            socket = IO.socket(String.format("%s:%s/", this.__SOCKET__Endpoint, this.__SOCKET__Port), options);
                            if(socket != null) {
                                this._Socket = socket;
                                socket.on(SOCKET_EVENT__TYPING, payload -> {
                                    if (payload.length != 0) {
                                        try {
                                            JSONObject payloadJson = (JSONObject) payload[0];
                                    /*  {   client : {account_id: 82, name: "Gianni"},
                                            conversation : {conversation_id: "173922", account_id: 82, user_id: 55722, is_note: false},
                                            is_typing : "n" } */
                                            JSONObject client = payloadJson.optJSONObject("client");
                                            if (client != null) {
                                                long account_id = client.optLong("account_id", -1L);
                                                if (account_id != -1L) {
                                                    try {
                                                        this._DEV_log(String.format("SOCKET RX: %1$s -> %2$s", SOCKET_EVENT__TYPING, payloadJson.toString(1)));
                                                    } catch (JSONException ignored) {
                                                    }
                                                    boolean is_typing = "y".equals(payloadJson.optString("is_typing"));
                                                    payloadJson = payloadJson.getJSONObject("conversation");
                                                    if (payloadJson != null) {
                                                        IE_JwtToken token2 = this._JwtToken;
                                                        if (token2 != null && token2._UserID != null && token2._UserID == payloadJson.getLong("user_id") && !payloadJson.optBoolean("is_note", false)) {
                                                            long conversation_id = payloadJson.optLong("conversation_id", 0);
                                                            __SOCKET__ITyping_listener listener = this.__SOCKET__Typing_listener;
                                                            if (conversation_id != 0 && listener != null) {
                                                                listener.onTypingEvent(conversation_id, account_id, is_typing);
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                        } catch (JSONException ignored) {
                                        }
                                    }
                                });
                                socket.on(SOCKET_EVENT__MESSAGE, payload -> {
                                    if (payload.length != 0) {
                                        try {
                                            JSONObject payloadJson = (JSONObject) payload[0];
                                    /*
                                    {user_id: 41897, account_id: 82, timestamp: 1483388854, from_account: true,
                                        conversation : {is_note: false} }
                                     */
                                            if (payloadJson.optBoolean("from_account")) {
                                                try {
                                                    this._log("Message received via socket");
                                                    this._DEV_log(String.format("SOCKET RX: %1$s -> %2$s", SOCKET_EVENT__MESSAGE, payloadJson.toString(1)));
                                                } catch (JSONException ignored) {
                                                }
                                                long timestamp = payloadJson.optLong("timestamp", 0L);
                                                long socket_user_id = payloadJson.optLong("user_id", 0L);

                                                IE_JwtToken token2 = this._JwtToken;
                                                if (token2 != null && token2._UserID != null && token2._UserID == socket_user_id
                                                        && socket_user_id != 0 && timestamp != 0
                                                        && !payloadJson.getJSONObject("conversation").optBoolean("is_note", false)) {
                                                    new IApi_Request.Builder<ArrayList<IE_Message>>(IApi_Request.ENDPOINT_MESSAGE_NEWS)
                                                            .opt_converter(data -> IU_Utils.fromJSONdataToList(data, "messages", IE_Message::new))
                                                            .opt_tokenMandatory()
                                                            .opt_receiver((responseState, new_messages) -> {
                                                                if (responseState == IApi_Request.RESPONSE_STATE__OK && new_messages != null && new_messages.size() != 0) {
                                                                    Activity activity = _CurrentActivity == null ? null : _CurrentActivity.get();
                                                                    if (activity != null) {
                                                                        if (activity instanceof SDKActivity) {
                                                                            ((SDKActivity) activity).onNewSocketMessages(new_messages);
                                                                        } else if (this._SupportEnabled) {
                                                                            try {
                                                                                PW_AlertMessage.show(activity, new_messages.get(0));
                                                                            } catch (WindowManager.BadTokenException changedActivityWhileExecuting) {
                                                                                activity = _CurrentActivity == null ? null : _CurrentActivity.get();
                                                                                if (activity != null) {
                                                                                    if (activity instanceof SDKActivity) {
                                                                                        ((SDKActivity) activity).onNewSocketMessages(new_messages);
                                                                                    } else {
                                                                                        try {
                                                                                            PW_AlertMessage.show(activity, new_messages.get(0));
                                                                                        } catch (WindowManager.BadTokenException ignored) {
                                                                                            //Second try failure.
                                                                                        }
                                                                                    }
                                                                                }
                                                                            }
                                                                        }
                                                                    }
                                                                }
                                                            })
                                                            .param("timestamp", timestamp)
                                                            .start();
                                                }
                                            }
                                        } catch (JSONException ignored) {
                                        }
                                    }
                                });

                                socket.on(Socket.EVENT_DISCONNECT, p -> this.__SOCKET__check());

                                socket.connect();
                                this.__Handler.postDelayed(this.__SOCKET__ping, SOCKET_PING_INTERVAL);
                            }
                        } catch (Exception ignored) { }
                    }
                }
            }
        }
    }
    private void __SOCKET__check() {
        if(this.__SOCKET__shouldBeConnected) {
            Socket socket = this._Socket;
            if(socket == null || !socket.connected()) {
                this.__SOCKET__connect(null);
            }
        }
    }
    private void __SOCKET__disconnect(@Nullable Socket socket) {
        this.__SOCKET__shouldBeConnected = false;
        if (socket != null) {
            if (socket.connected()) {
                socket.disconnect();
            }
            this._Socket = null;
            this.__SOCKET__CurrentConfiguration = null;
        }
        this.__Handler.removeCallbacks(this.__SOCKET__ping);
    }
    private void __SOCKET__SEND(@NonNull String event, @NonNull JSONObject payloadJson) {
        this.__SOCKET__check();
        Socket socket = this._Socket;
        if(socket != null) {
            try {
                this._DEV_log(String.format("SOCKET TX: %1$s -> %2$s", event, payloadJson.toString(1)));
            } catch (JSONException ignored) {
            }
            socket.emit(event, payloadJson);
        }
    }

    void __SOCKET_SEND_Typing(long pConversationID, boolean pTyping, @Nullable String pText) {
        //{conversation: {conversation_id: 179170, user_id: 63378, is_note: false}, is_typing: "y", typing_preview: "I am writ"}
        IE_JwtToken token = this._JwtToken;
        if(token != null && token._UserID != null) {
            try {
                this.__SOCKET__SEND(SOCKET_EVENT__TYPING, new JSONObject()
                        .put("conversation", new JSONObject()
                                .put("conversation_id", pConversationID)
                                .put("user_id", token._UserID)
                                .put("is_note", false))
                        .put("is_typing", pTyping ? "y" : "n")
                        .put("typing_preview", pText));
            } catch (JSONException ignored) { }
        }
    }
    void __SOCKET_SEND_Message(long pTimestamp) {
        if(pTimestamp != -1L) {
            IE_JwtToken token = this._JwtToken;
            if (token != null && token._UserID != null) {
{'fraction_non_alphanumeric': 0.07409312267956733, 'fraction_numerical': 0.004098426525659365, 'mean_word_length': 1.255083179297597, 'pattern_counts': {'":': 6, '<': 25, '<?xml version=': 0, '>': 46, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 550, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1463329', 'n_tokens_mistral': 13714, 'n_tokens_neox': 13094, 'n_words': 2702}