package com.eveningoutpost.dexdrip;


import android.app.Activity;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.os.PowerManager;
import android.preference.PreferenceManager;
import android.support.v4.content.LocalBroadcastManager;
import android.widget.Toast;

import com.eveningoutpost.dexdrip.Models.BgReading;
import com.eveningoutpost.dexdrip.Models.BloodTest;
import com.eveningoutpost.dexdrip.Models.Calibration;
import com.eveningoutpost.dexdrip.Models.DesertSync;
import com.eveningoutpost.dexdrip.Models.JoH;
import com.eveningoutpost.dexdrip.Models.RollCall;
import com.eveningoutpost.dexdrip.Models.Sensor;
import com.eveningoutpost.dexdrip.Models.Treatments;
import com.eveningoutpost.dexdrip.Models.UserError;
import com.eveningoutpost.dexdrip.Models.UserError.Log;
import com.eveningoutpost.dexdrip.Services.PlusSyncService;
import com.eveningoutpost.dexdrip.UtilityModels.Constants;
import com.eveningoutpost.dexdrip.UtilityModels.InstalledApps;
import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore;
import com.eveningoutpost.dexdrip.UtilityModels.Pref;
import com.eveningoutpost.dexdrip.utils.CipherUtils;
import com.eveningoutpost.dexdrip.utils.DisplayQRCode;
import com.eveningoutpost.dexdrip.utils.SdcardImportExport;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.gcm.GoogleCloudMessaging;
import com.google.common.primitives.Bytes;
import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.RemoteMessage;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.Expose;
import com.google.gson.internal.bind.DateTypeAdapter;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import static com.eveningoutpost.dexdrip.xdrip.gs;

/**
 * Created by jamorham on 11/01/16.
 */


public class GcmActivity extends FauxActivity {

    private static final int PLAY_SERVICES_RESOLUTION_REQUEST = 9000;
    static final String TASK_TAG_CHARGING = "charging";
    static final String TASK_TAG_UNMETERED = "unmetered";
    private static final String TAG = "jamorham gcmactivity";
    public static long last_sync_request = 0;
    public static long last_sync_fill = 0;
    private static int bg_sync_backoff = 0;
    private static long last_ping_request = 0;
    private static long last_rlcl_request = 0;
    private static long cool_down_till = 0;
    public static AtomicInteger msgId = new AtomicInteger(1);
    public static String token = null;
    public static String senderid = null;
    public static final List<GCM_data> gcm_queue = new ArrayList<>();
    private static final Object queue_lock = new Object();
    private BroadcastReceiver mRegistrationBroadcastReceiver;
    public static boolean cease_all_activity = false;
    private static boolean cease_all_checked = false;
    public static volatile long last_ack = -1;
    public static volatile long last_send = -1;
    public static volatile long last_send_previous = -1;
    private static final long MAX_ACK_OUTSTANDING_MS = 3600000;
    private static int recursion_depth = 0;
    private static int last_bridge_battery = -1;
    private static int last_parakeet_battery = -1;
    private static final int MAX_RECURSION = 30;
    private static final int MAX_QUEUE_SIZE = 300;
    private static final int RELIABLE_MAX_PAYLOAD = 1800;
    private static final int RELIABLE_MAX_BINARY_PAYLOAD = 1400;
    private static final boolean d = false; // debug


    private static SensorCalibrations[] getSensorCalibrations(String json) {
        SensorCalibrations[] sensorCalibrations = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create().fromJson(json, SensorCalibrations[].class);
        Log.d(TAG, "After fromjson sensorCalibrations are " + sensorCalibrations.toString());
        return sensorCalibrations;
    }

    private static String sensorAndCalibrationsToJson(Sensor sensor, int limit) {
        SensorCalibrations[] sensorCalibrations = new SensorCalibrations[1];
        sensorCalibrations[0] = new SensorCalibrations();
        sensorCalibrations[0].sensor = sensor;
        sensorCalibrations[0].calibrations = Calibration.getCalibrationsForSensor(sensor, limit);
        if (d) Log.d(TAG, "calibrations size " + sensorCalibrations[0].calibrations.size());
        Gson gson = new GsonBuilder()
                .excludeFieldsWithoutExposeAnnotation()
                .registerTypeAdapter(Date.class, new DateTypeAdapter())
                .serializeSpecialFloatingPointValues()
                .create();

        String output = gson.toJson(sensorCalibrations);
        if (d) Log.d(TAG, "sensorAndCalibrationsToJson created the string " + output);
        return output;
    }

    static NewCalibration getNewCalibration(String json) {
        NewCalibration[] newCalibrationArray = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create().fromJson(json, NewCalibration[].class);
        if (newCalibrationArray != null) {
            Log.e(TAG, "After fromjson NewCalibration are " + newCalibrationArray.toString());
        } else {
            Log.e(TAG, "Error creating newCalibrationArray");
            return null;
        }
        return newCalibrationArray[0];
    }

    private static String newCalibrationToJson(double bgValue, String uuid, long offset) {
        NewCalibration newCalibrationArray[] = new NewCalibration[1];
        NewCalibration newCalibration = new NewCalibration();
        newCalibration.bgValue = bgValue;
        newCalibration.uuid = uuid;
        newCalibration.timestamp = JoH.tsl();
        newCalibration.offset = offset;
        newCalibrationArray[0] = newCalibration;

        Gson gson = new GsonBuilder()
                .excludeFieldsWithoutExposeAnnotation()
                .registerTypeAdapter(Date.class, new DateTypeAdapter())
                .serializeSpecialFloatingPointValues()
                .create();

        String output = gson.toJson(newCalibrationArray);
        Log.d(TAG, "newCalibrationToJson Created the string " + output);
        return output;
    }

    static void upsertSensorCalibratonsFromJson(String json) {
        Log.i(TAG, "upsertSensorCalibratonsFromJson called");
        SensorCalibrations[] sensorCalibrations = getSensorCalibrations(json);
        for (SensorCalibrations SensorCalibration : sensorCalibrations) {
            Sensor.upsertFromMaster(SensorCalibration.sensor);
            for (Calibration calibration : SensorCalibration.calibrations) {
                Log.d(TAG, "upsertSensorCalibratonsFromJson updating calibration " + calibration.uuid);
                Calibration.upsertFromMaster(calibration);
            }
        }
    }

    static synchronized void queueAction(String reference) {
        synchronized (queue_lock) {
            Log.d(TAG, "Received ACK, Queue Size: " + GcmActivity.gcm_queue.size() + " " + reference);
            last_ack = JoH.tsl();
            for (GCM_data datum : gcm_queue) {
                String thisref = datum.bundle.getString("action") + datum.bundle.getString("payload");
                if (thisref.equals(reference)) {
                    gcm_queue.remove(gcm_queue.indexOf(datum));
                    Log.d(TAG, "Removing acked queue item: " + reference);
                    break;
                }
            }
            queueCheckOld(xdrip.getAppContext());
        }
    }

    static void queueCheckOld(Context context) {
        queueCheckOld(context, false);
    }

    private static void queueCheckOld(Context context, boolean recursive) {

        if (context == null) {
            Log.e(TAG, "Can't process old queue as null context");
            return;
        }

        if (overHeated()) {
            Log.e(TAG, "Can't process old queue as in cool down state");
            return;
        }

        final long MAX_QUEUE_AGE = (5 * 60 * 60 * 1000); // 5 hours
        final long MIN_QUEUE_AGE = (15000);
        final long MAX_RESENT = 10;
        final long timenow = JoH.tsl();
        boolean queuechanged = false;
        if (!recursive) recursion_depth = 0;
        synchronized (queue_lock) {
            for (GCM_data datum : gcm_queue) {
                if (datum != null) {
                    if (overHeated()) break;
                    if ((timenow - datum.timestamp) > MAX_QUEUE_AGE
                            || datum.resent > MAX_RESENT) {
                        queuechanged = true;
                        Log.i(TAG, "Removing old unacknowledged queue item: resent: " + datum.resent);
                        gcm_queue.remove(gcm_queue.indexOf(datum));
                        break;
                    } else if (timenow - datum.timestamp > MIN_QUEUE_AGE) {
                        try {
                            Log.i(TAG, "Resending unacknowledged queue item: " + datum.bundle.getString("action") + datum.bundle.getString("payload"));
                            datum.resent++;
                            GoogleCloudMessaging.getInstance(context).send(senderid + "@gcm.googleapis.com", Integer.toString(msgId.incrementAndGet()), datum.bundle);
                        } catch (Exception e) {
                            Log.e(TAG, "Got exception during resend: " + e.toString());
                        }
                        break;
                    }
                } else {
                    UserError.Log.wtf(TAG, "Null datum in gcm_queue - should be impossible!");
                    break;
                }
            }
        }
        if (queuechanged) {
            recursion_depth++;
            if (recursion_depth < MAX_RECURSION) {
                queueCheckOld(context, true);
            } else {
                Log.e(TAG, "Max recursion exceeded!");
            }
        }
    }

    private static void checkCease() {
        if ((!cease_all_checked) && (!cease_all_activity)) {
            cease_all_activity = Pref.getBooleanDefaultFalse("disable_all_sync");
            cease_all_checked = true;
        }
    }

    private static String sendMessage(final String action, final String payload) {
        return sendMessage(myIdentity(), action, payload);
    }

    private static String sendMessage(final String action, final byte[] bpayload) {
        return sendMessage(myIdentity(), action, bpayload);
    }

    private static String sendMessage(final String identity, final String action, final String payload) {
        checkCease();
        if (cease_all_activity) return null;
        if (identity == null) return null;
        new Thread() {
            @Override
            public void run() {
                sendMessageNow(identity, action, payload, null);
            }
        }.start();
        return "sent async";
    }

    private static String sendMessage(final String identity, final String action, final byte[] bpayload) {
        checkCease();
        if (cease_all_activity) return null;
        if (identity == null) return null;
        new Thread() {
            @Override
            public void run() {
                sendMessageNow(identity, action, "", bpayload);
            }
        }.start();
        return "sent async";
    }

    public synchronized static void syncBGReading(BgReading bgReading) {
        if (bgReading == null) {
            UserError.Log.wtf(TAG, "Cannot sync null bgreading - should never occur");
            return;
        }
        Log.d(TAG, "syncBGReading called");
        if (JoH.ratelimit("gcm-bgs-batch", 15)) {
            GcmActivity.sendMessage("bgs", bgReading.toJSON(true));
        } else {
            PersistentStore.appendBytes("gcm-bgs-batch-queue", bgReading.toMessage());
            PersistentStore.setLong("gcm-bgs-batch-time", JoH.tsl());
            processBgsBatch(false);
        }
    }

    // called only from interactive or evaluated new data
    public synchronized static void syncBloodTests() {
        Log.d(TAG, "syncBloodTests called");
        if (Home.get_master_or_follower()) {
            if (JoH.ratelimit("gcm-btmm-send", 4)) {
                final byte[] this_btmm = BloodTest.toMultiMessage(BloodTest.last(12));
                if (JoH.differentBytes("gcm-btmm-last-send", this_btmm)) {
                    sendMessage("btmm", JoH.compressBytesforPayload(this_btmm));
                    Home.staticRefreshBGCharts();
                } else {
                    Log.d(TAG, "btmm message is identical to previously sent");
                }
            }
        }
    }

    private synchronized static void processBgsBatch(boolean send_now) {
        final byte[] value = PersistentStore.getBytes("gcm-bgs-batch-queue");
        Log.d(TAG, "Processing BgsBatch: length: " + value.length + " now:" + send_now);
        if ((send_now) || (value.length > (RELIABLE_MAX_BINARY_PAYLOAD - 100))) {
            if (value.length > 0) {
                PersistentStore.setString("gcm-bgs-batch-queue", "");
                GcmActivity.sendMessage("bgmm", value);
            }
            Log.d(TAG, "Sent batch");
        } else {
            JoH.runOnUiThreadDelayed(new Runnable() {
                @Override
                public void run() {
                    if (JoH.msSince(PersistentStore.getLong("gcm-bgs-batch-time")) > 4000) {
                        Log.d(TAG, "Progressing BGSbatch due to timeout");
                        processBgsBatch(true);
                    }
                }
            }, 5000);
        }
    }

    public static synchronized void syncSensor(Sensor sensor, boolean forceSend) {
        Log.d(TAG, "syncsensor backtrace: " + JoH.backTrace());
        Log.i(TAG, "syncSensor called");
        if (sensor == null) {
            Log.e(TAG, "syncSensor sensor is null");
            return;
        }
        if ((!forceSend) && !JoH.pratelimit("GcmSensorCalibrationsUpdate", 300)) {
            Log.i(TAG, "syncSensor not sending data, because of rate limiter");
            return;
        }

        // automatically find a suitable volume of payload data
        for (int limit = 9; limit > 0; limit--) {
            final String json = sensorAndCalibrationsToJson(sensor, limit);
            if (d)
                Log.d(TAG, "sensor json size: limit: " + limit + " len: " + CipherUtils.compressEncryptString(json).length());
            if (CipherUtils.compressEncryptString(json).length() <= RELIABLE_MAX_PAYLOAD) {
                final String json_hash = CipherUtils.getSHA256(json);
                if (!forceSend || !PersistentStore.getString("last-syncsensor-json").equals(json_hash)) {
                    PersistentStore.setString("last-syncsensor-json", json_hash);
                    GcmActivity.sendMessage(GcmActivity.myIdentity(), "sensorupdate", json);
                } else {
                    Log.d(TAG, "syncSensor: data is duplicate of last data: " + json);
                    break;
                }
                break; // send only one
            }
        }
    }

    public static void requestPing() {
        if ((JoH.tsl() - last_ping_request) > (60 * 1000 * 15)) {
            last_ping_request = JoH.tsl();
            Log.d(TAG, "Sending ping");
            if (JoH.pratelimit("gcm-ping", 1199))
                GcmActivity.sendMessage("ping", new RollCall().populate().toS());
        } else {
            Log.d(TAG, "Already requested ping recently");
        }
    }

    public static void desertPing() {
        if (JoH.pratelimit("gcm-desert-ping", 300)) {
            GcmActivity.sendMessage("ping", new RollCall().populate().toS());
        } else {
            Log.d(TAG, "Already requested desert ping recently");
        }
    }

    public static void requestRollCall() {
        if (JoH.tsl() - last_rlcl_request > (60 * 1000)) {
            last_rlcl_request = JoH.tsl();
            if (JoH.pratelimit("gcm-rlcl", 3600))
                GcmActivity.sendMessage("rlcl", new RollCall().populate().toS());
        }
    }

    static void sendLocation(final String location) {
        if (JoH.pratelimit("gcm-plu", 180)) {
            GcmActivity.sendMessage("plu", location);
        }
    }

    public static void sendSensorBattery(final int battery) {
        if (JoH.pratelimit("gcm-sbu", 3600)) {
            GcmActivity.sendMessage("sbu", Integer.toString(battery));
        }
    }

    public static void sendBridgeBattery(final int battery) {
        if (battery != last_bridge_battery) {
            if (JoH.pratelimit("gcm-bbu", 1800)) {
                GcmActivity.sendMessage("bbu", Integer.toString(battery));
                last_bridge_battery = battery;
            }
        }
    }

    public static void sendParakeetBattery(final int battery) {
        if (battery != last_parakeet_battery) {
            if (JoH.pratelimit("gcm-pbu", 1800)) {
                GcmActivity.sendMessage("pbu", Integer.toString(battery));
                last_parakeet_battery = battery;
            }
        }
    }

    public static void sendNotification(String title, String message) {
        if (JoH.pratelimit("gcm-not", 30)) {
            GcmActivity.sendMessage("not", title.replaceAll("\\^", "") + "^" + message.replaceAll("\\^", ""));
        }
    }

    private static void sendRealSnoozeToRemote() {
        if (JoH.pratelimit("gcm-sra", 60)) {
            String wifi_ssid = JoH.getWifiSSID();
            if (wifi_ssid == null) wifi_ssid = "";
            sendMessage("sra", Long.toString(JoH.tsl()) + "^" + JoH.base64encode(wifi_ssid));
        }
    }

    public static void sendSnoozeToRemote() {
        if ((Home.get_master() || Home.get_follower()) && (Pref.getBooleanDefaultFalse("send_snooze_to_remote"))
                && (JoH.pratelimit("gcm-sra-maybe", 5))) {
            if (Pref.getBooleanDefaultFalse("confirm_snooze_to_remote")) {
                Home.startHomeWithExtra(xdrip.getAppContext(), Home.HOME_FULL_WAKEUP, "1");
                Home.startHomeWithExtra(xdrip.getAppContext(), Home.SNOOZE_CONFIRM_DIALOG, "");
            } else {
                sendRealSnoozeToRemote();
                UserError.Log.ueh(TAG, "Sent snooze to remote");
            }
        }
    }

    static void sendSnoozeToRemoteWithConfirm(final Context context) {
        final long when = JoH.tsl();
        final AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setTitle(xdrip.getAppContext().getString(R.string.confirm_remote_snooze));
        builder.setMessage(xdrip.getAppContext().getString(R.string.are_you_sure_you_wish_to_snooze_all_other_devices_in_your_sync_group));
        builder.setPositiveButton(xdrip.getAppContext().getString(R.string.yes_send_it), new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
                if ((JoH.tsl() - when) < 120000) {
                    sendRealSnoozeToRemote();
                    UserError.Log.ueh(TAG, "Sent snooze to remote after confirmation");
                } else {
                    JoH.static_toast_long("Took too long to confirm! Ignoring!");
                    UserError.Log.ueh(TAG, "Ignored snooze confirmation as took > 2 minutes to confirm!");
                }
            }
        });

        builder.setNegativeButton(xdrip.getAppContext().getString(R.string.no), new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
            }
        });
        final AlertDialog alert = builder.create();
        alert.show();
        // Hide after some seconds
        final Handler handler = new Handler();
        final Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    if (alert.isShowing()) {
                        alert.dismiss();
                    }
                } catch (IllegalArgumentException e) {
                    Log.e(TAG, "Got exception trying to auto-dismiss dialog: " + e);
                }
            }
        };

        alert.setOnDismissListener(new DialogInterface.OnDismissListener() {
            @Override
            public void onDismiss(DialogInterface dialog) {
                handler.removeCallbacks(runnable);
            }
        });

        handler.postDelayed(runnable, 120000);


    }

    public static void sendMotionUpdate(final long timestamp, final int activity) {
        if (JoH.pratelimit("gcm-amu", 5)) {
            sendMessage("amu", Long.toString(timestamp) + "^" + Integer.toString(activity));
        }
    }

    public static void sendPumpStatus(String json) {
        if (JoH.pratelimit("gcm-psu", 180)) {
            sendMessage("psu", json);
        }
    }

    public static void sendNanoStatusUpdate(final String prefix, final String json) {
        if (JoH.pratelimit("gcm-nscu" + prefix, 30)) {
            UserError.Log.d(TAG, "Sending nano status update: " + prefix + " " + json);
            sendMessage("nscu" + prefix, json);
        }
    }

    public static void sendMimeoGraphUpdate(final String json) {
        if (JoH.pratelimit("gcm-mimg", 180)) {
            UserError.Log.d(TAG, "Sending mimeograph key update: " + json);
            sendMessage("mimg", json);
        }
    }


    public static void requestBGsync() {
        if (token != null) {
            if ((JoH.tsl() - last_sync_request) > (60 * 1000 * (5 + bg_sync_backoff))) {
                last_sync_request = JoH.tsl();
                final BgReading bgReading = BgReading.last();
                if (JoH.pratelimit("gcm-bfr", 299)) {
                    GcmActivity.sendMessage("bfr", bgReading != null ? "" + bgReading.timestamp : "");
                }
                bg_sync_backoff++;
            } else {
                Log.d(TAG, "Already requested BGsync recently, backoff: " + bg_sync_backoff);
                if (JoH.ratelimit("check-queue", 20)) {
                    queueCheckOld(xdrip.getAppContext());
                }
            }
        } else {
            Log.d(TAG, "No token for BGSync");
        }
    }

    static synchronized void syncBGTable2() {
        if (!Sensor.isActive()) return;
        new Thread() {
            @Override
            public void run() {
                final PowerManager.WakeLock wl = JoH.getWakeLock("syncBGTable", 300000);
                //if ((JoH.ts() - last_sync_fill) > (60 * 1000 * (5 + bg_sync_backoff))) {
                if (JoH.pratelimit("last-sync-fill", 60 * (5 + bg_sync_backoff))) {
                    last_sync_fill = JoH.tsl();
                    bg_sync_backoff++;

                    // Since this is a big update, also update sensor and calibrations
                    syncSensor(Sensor.currentSensor(), true);

                    final List<BgReading> bgReadings = BgReading.latestForGraph(300, JoH.tsl() - (24 * 60 * 60 * 1000));

                    StringBuilder stringBuilder = new StringBuilder();
                    for (BgReading bgReading : bgReadings) {
                        String myrecord = bgReading.toJSON(false);
                        if (stringBuilder.length() > 0) {
                            stringBuilder.append("^");
                        }
                        stringBuilder.append(myrecord);
                    }
                    final String mypacket = stringBuilder.toString();
                    Log.d(TAG, "Total BGreading sync packet size: " + mypacket.length());
                    if (mypacket.length() > 0) {
                        DisplayQRCode.uploadBytes(mypacket.getBytes(Charset.forName("UTF-8")), 2);
                    } else {
                        Log.i(TAG, "Not uploading data due to zero length");
                    }
                } else {
                    Log.d(TAG, "Ignoring recent sync request, backoff: " + bg_sync_backoff);
                }
                JoH.releaseWakeLock(wl);
            }
        }.start();
    }

    // callback function
    public static void backfillLink(String id, String key) {
        Log.d(TAG, "sending bfb message: " + id);
        sendMessage("bfb", id + "^" + key);
    }

    static void processBFPbundle(String bundle) {
        String[] bundlea = bundle.split("\\^");
        for (String bgr : bundlea) {
            BgReading.bgReadingInsertFromJson(bgr, false);
        }
        GcmActivity.requestSensorBatteryUpdate();
        Home.staticRefreshBGCharts();
    }

    static void requestSensorBatteryUpdate() {
        if (Home.get_follower() && JoH.pratelimit("SensorBatteryUpdateRequest", 1200)) {
            Log.d(TAG, "Requesting Sensor Battery Update");
            GcmActivity.sendMessage("sbr", ""); // request sensor battery update
        }
    }

    public static void requestSensorCalibrationsUpdate() {
        if (Home.get_follower() && JoH.pratelimit("SensorCalibrationsUpdateRequest", 300)) {
            Log.d(TAG, "Requesting Sensor and calibrations Update");
            GcmActivity.sendMessage("sensor_calibrations_update", "");
        }
    }

    public static void pushTreatmentAsync(final Treatments thistreatment) {
        if ((thistreatment.uuid == null) || (thistreatment.uuid.length() < 5)) return;
        final String json = thistreatment.toJSON();
        sendMessage(myIdentity(), "nt", json);
    }

    static void send_ping_reply() {
        Log.d(TAG, "Sending ping reply");
        sendMessage(myIdentity(), "q", "");
    }

    public static void push_delete_all_treatments() {
        Log.i(TAG, "Sending push for delete all treatments");
        sendMessage(myIdentity(), "dat", "");
    }

    public static void push_delete_treatment(Treatments treatment) {
        Log.i(TAG, "Sending push for specific treatment");
        sendMessage(myIdentity(), "dt", treatment.uuid);
    }

    public static void push_stop_master_sensor() {
        sendMessage("ssom", "challenge string");
    }

    public static void push_start_master_sensor() {
        sendMessage("rsom", JoH.tsl() + "");
    }

    public static void push_external_status_update(long timestamp, String statusLine) {
        if (JoH.ratelimit("gcm-esup", 30)) {
            sendMessage("esup", timestamp + "^" + statusLine);
        }
    }

    static String myIdentity() {
        // TODO prefs override possible
        return GoogleDriveInterface.getDriveIdentityString();
    }

    static void pushTreatmentFromPayloadString(String json) {
        if (json.length() < 3) return;
        Log.d(TAG, "Pushing json from GCM: " + json);
        Treatments.pushTreatmentFromJson(json);
    }

    static void pushCalibration(String bg_value, String seconds_ago) {
        if ((bg_value.length() == 0) || (seconds_ago.length() == 0)) return;
        if (Home.get_master()) {
            // For master, we now send the entire table, no need to send this specific table each time
            return;
        }
        if (Home.get_follower()) {
            final String currenttime = Double.toString(new Date().getTime());
            final String tosend = currenttime + " " + bg_value + " " + seconds_ago;
            sendMessage(myIdentity(), "cal", tosend);
        }
    }

    static void pushCalibration2(double bgValue, String uuid, long offset) {
        Log.i(TAG, "pushCalibration2 called: " + JoH.qs(bgValue, 1) + " " + uuid + " " + offset);
        if (Home.get_master_or_follower()) {
            final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(xdrip.getAppContext());
            final String unit = prefs.getString("units", "mgdl");

            if (unit.compareTo("mgdl") != 0) {
                bgValue = bgValue * Constants.MMOLL_TO_MGDL;
            }

            if ((bgValue < 40) || (bgValue > 400)) {
                Log.wtf(TAG, "Invalid out of range calibration glucose mg/dl value of: " + bgValue);
                JoH.static_toast_long("Calibration out of range: " + bgValue + " mg/dl");
                return;
            }
            final String json = newCalibrationToJson(bgValue, uuid, offset);
            GcmActivity.sendMessage(myIdentity(), "cal2", json);
        }
    }

    public static void clearLastCalibration(String uuid) {
        sendMessage(myIdentity(), "clc", uuid);
    }

    private static synchronized String sendMessageNow(String identity, String action, String payload, byte[] bpayload) {

        Log.i(TAG, "Sendmessage called: " + identity + " " + action + " " + payload);
        String msg;
        try {

            if (xdrip.getAppContext() == null) {
                Log.e(TAG, "mContext is null cannot sendMessage");
                return "";
            }

            if (identity == null) {
                Log.e(TAG, "identity is null cannot sendMessage");
                return "";
            }

            if (overHeated()) {
                UserError.Log.e(TAG, "Cannot send message due to cool down period: " + action + " till: " + JoH.dateTimeText(cool_down_till));
                return "";
            }

            final Bundle data = new Bundle();
            data.putString("action", action);
            data.putString("identity", identity);

            if (action.equals("sensorupdate")) {
                final String ce_payload = CipherUtils.compressEncryptString(payload);
                Log.i(TAG, "sensor length CipherUtils.encryptBytes ce_payload length: " + ce_payload.length());
                data.putString("payload", ce_payload);
                if (d) Log.d(TAG, "sending data len " + ce_payload.length() + " " + ce_payload);
            } else {
                if ((bpayload != null) && (bpayload.length > 0)) {
                    data.putString("payload", CipherUtils.encryptBytesToString(Bytes.concat(bpayload, JoH.bchecksum(bpayload)))); // don't double sum
                } else if (payload.length() > 0) {
                    data.putString("payload", CipherUtils.encryptString(payload));
                } else {
                    data.putString("payload", "");
                }
            }

            if (gcm_queue.size() < MAX_QUEUE_SIZE) {
                if (shouldAddQueue(data)) {
                    gcm_queue.add(new GCM_data(data));
                }
            } else {
                Log.e(TAG, "Queue size exceeded");
                Home.toaststaticnext("Maximum Sync Queue size Exceeded!");
            }
            final GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(xdrip.getAppContext());
            if (token == null) {
                Log.e(TAG, "GCM token is null - cannot sendMessage");
                return "";
            }
            String messageid = Integer.toString(msgId.incrementAndGet());
            gcm.send(senderid + "@gcm.googleapis.com", messageid, data);
            if (last_ack == -1) last_ack = JoH.tsl();
            last_send_previous = last_send;
            last_send = JoH.tsl();
            msg = "Sent message OK " + messageid;
            DesertSync.fromGCM(data);
        } catch (IOException ex) {
            msg = "Error :" + ex.getMessage();
        }
        Log.d(TAG, "Return msg in SendMessage: " + msg);
        return msg;
    }

    private static boolean shouldAddQueue(Bundle data) {
        final String action = data.getString("action");
        if (action == null) return false;
        switch (action) {
            // one shot action types where multi queuing is not needed
            case "ping":
            case "rlcl":
            case "sbr":
            case "bfr":
                synchronized (queue_lock) {
                    for (GCM_data qdata : gcm_queue) {
                        try {
                            if (qdata.bundle.getString("action").equals(action)) {
                                Log.d(TAG, "Skipping queue add for duplicate action: " + action);
                                return false;
                            }
                        } catch (NullPointerException e) {
                            //
                        }
                    }
                }
                return true;
            default:
                return true;
        }
    }

    private static void fmSend(Bundle data) {
        final FirebaseMessaging fm = FirebaseMessaging.getInstance();
        if (senderid != null) {
            fm.send(new RemoteMessage.Builder(senderid + "@gcm.googleapis.com")
                    .setMessageId(Integer.toString(msgId.incrementAndGet()))
                    .setData(JoH.bundleToMap(data))
                    .build());
        } else {
            Log.wtf(TAG, "senderid is null");
        }
    }

    private void tryGCMcreate() {
        Log.d(TAG, "try GCMcreate");
        checkCease();
        if (cease_all_activity) return;

        if (!InstalledApps.isGooglePlayInstalled(xdrip.getAppContext())) {
            if (JoH.pratelimit("gms-missing-msg", 86400)) {
                final String msg = "Google Play services - not installed!\nInstall it or disable xDrip+ sync options";
                JoH.static_toast_long(msg);
                Home.toaststaticnext(msg);
            }
            cease_all_activity = true;
            return;
        }

        mRegistrationBroadcastReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {

                SharedPreferences sharedPreferences =
                        PreferenceManager.getDefaultSharedPreferences(context);
                boolean sentToken = sharedPreferences
                        .getBoolean(PreferencesNames.SENT_TOKEN_TO_SERVER, false);
                if (sentToken) {
                    Log.i(TAG, "Token retrieved and sent");
                } else {
                    Log.e(TAG, "Error with token");
                }
            }
        };

        final Boolean play_result = checkPlayServices();
        if (play_result == null) {
            Log.d(TAG, "Indeterminate result for play services");
            PlusSyncService.backoff_a_lot();
        } else if (play_result) {
            final Intent intent = new Intent(xdrip.getAppContext(), RegistrationIntentService.class);
            xdrip.getAppContext().startService(intent);
        } else {
            cease_all_activity = true;
            final String msg = "ERROR: Connecting to Google Services - check google login or reboot?";
            JoH.static_toast_long(msg);
            Home.toaststaticnext(msg);
        }
    }

    // for starting FauxActivity
    public void jumpStart() {
        Log.d(TAG, "jumpStart() called");
        if (JoH.ratelimit("gcm-jumpstart", 5)) {
            onCreate(null);
        } else {
            Log.d(TAG, "Ratelimiting jumpstart");
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        try {
            super.onCreate(savedInstanceState);
            if (Pref.getBooleanDefaultFalse("disable_all_sync")) {
                cease_all_activity = true;
                Log.d(TAG, "Sync services disabled");
            }
            if (cease_all_activity) {
                finish();
                return;
            }
            Log.d(TAG, "onCreate");
            tryGCMcreate();
        } catch (Exception e) {
            Log.e(TAG, "Got exception in GCMactivity Oncreate: ", e);
        } finally {
            try {
                finish();
            } catch (Exception e) {
                Log.e(TAG, "Exception when finishing: " + e);
            }
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (cease_all_activity) return;
        LocalBroadcastManager.getInstance(xdrip.getAppContext()).registerReceiver(mRegistrationBroadcastReceiver,
                new IntentFilter(PreferencesNames.REGISTRATION_COMPLETE));
    }

    @Override
    protected void onPause() {
        try {
            LocalBroadcastManager.getInstance(xdrip.getAppContext()).unregisterReceiver(mRegistrationBroadcastReceiver);
        } catch (Exception e) {
            Log.e(TAG, "Exception onPause: ", e);
        }
        super.onPause();
    }

    static void checkSync(final Context context) {
        if ((GcmActivity.last_ack > -1) && (GcmActivity.last_send_previous > 0)) {
            if (GcmActivity.last_send_previous > GcmActivity.last_ack) {
                if (Pref.getLong("sync_warning_never", 0) == 0) {

                    if (PreferencesNames.SYNC_VERSION.equals("1") && JoH.isOldVersion(context)) {
                        final long since_send = JoH.tsl() - GcmActivity.last_send_previous;
                        if (since_send > 60000) {
                            if (!DesertSync.isEnabled()) {
                                final long ack_outstanding = JoH.tsl() - GcmActivity.last_ack;
                                if (ack_outstanding > MAX_ACK_OUTSTANDING_MS) {
                                    if (JoH.ratelimit("ack-failure", 7200)) {
                                        if (JoH.isAnyNetworkConnected()) {
                                            AlertDialog.Builder builder = new AlertDialog.Builder(context);
                                            builder.setTitle("Possible Sync Problem");
                                            builder.setMessage("It appears we haven't been able to send/receive sync data for the last: " + JoH.qs(ack_outstanding / 60000, 0) + " minutes\n\nDo you want to perform a reset of the sync system?");
                                            builder.setPositiveButton("YES, Do it!", new DialogInterface.OnClickListener() {
                                                public void onClick(DialogInterface dialog, int which) {
                                                    dialog.dismiss();
                                                    JoH.static_toast(context, "Resetting...", Toast.LENGTH_LONG);
                                                    SdcardImportExport.forceGMSreset();
                                                }
                                            });
                                            builder.setNeutralButton(gs(R.string.maybe_later), new DialogInterface.OnClickListener() {
                                                public void onClick(DialogInterface dialog, int which) {
                                                    dialog.dismiss();
                                                }
                                            });
                                            builder.setNegativeButton("NO, Never", new DialogInterface.OnClickListener() {
                                                @Override
                                                public void onClick(DialogInterface dialog, int which) {
                                                    dialog.dismiss();
                                                    Pref.setLong("sync_warning_never", JoH.tsl());
                                                }
                                            });
                                            AlertDialog alert = builder.create();
                                            alert.show();
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    static void coolDown() {
        cool_down_till = JoH.tsl() + Constants.MINUTE_IN_MS * 20;
        Log.wtf(TAG, "Too many messages, activating cool down till: " + JoH.dateTimeText(cool_down_till));
    }

    static boolean overHeated() {
        return (cool_down_till != 0 && JoH.msSince(cool_down_till) < 0);
    }

    /**
     * Check the device to make sure it has the Google Play Services APK. If
     * it doesn't, display a dialog that allows users to download the APK from
     * the Google Play Store or enable it in the device's system settings.
     */

    private static Boolean checkPlayServices() {
        return checkPlayServices(xdrip.getAppContext(), null);
    }

    static Boolean checkPlayServices(Context context, Activity activity) {
        checkCease();
        if (cease_all_activity) return false;
        final GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance();
        int resultCode = apiAvailability.isGooglePlayServicesAvailable(context);
        if (resultCode != ConnectionResult.SUCCESS) {
            try {
                if (apiAvailability.isUserResolvableError(resultCode)) {
                    if (activity != null) {
                        apiAvailability.getErrorDialog(activity, resultCode, PLAY_SERVICES_RESOLUTION_REQUEST)
                                .show();
                    } else {
                        if (JoH.ratelimit(Home.GCM_RESOLUTION_ACTIVITY, 60)) {
                            //apiAvailability.showErrorNotification(context, resultCode);
                            Home.startHomeWithExtra(context, Home.GCM_RESOLUTION_ACTIVITY, "1");
                            return null;
                        } else {
                            Log.e(TAG, "Ratelimit exceeded for " + Home.GCM_RESOLUTION_ACTIVITY);
                        }
                    }
                } else {
                    final String msg = "This device is not supported for play services.";
                    Log.i(TAG, msg);
                    JoH.static_toast_long(msg);
                    cease_all_activity = true;
                    return false;
                }
            } catch (Exception e) {
                Log.e(TAG, "Error resolving google play - probably no google");
                cease_all_activity = true;
            }
            return false;
        }
        return true;
    }

    private static class GCM_data {
        public Bundle bundle;
        public long timestamp;
        private int resent;

        private GCM_data(Bundle data) {
            bundle = data;
            timestamp = JoH.tsl();
            resent = 0;
        }
    }
}

class SensorCalibrations {
    @Expose
    Sensor sensor;

    @Expose
    List<Calibration> calibrations;
}

class NewCalibration {
    @Expose
    double bgValue; // Always in mgdl

    @Expose
    long timestamp;

    @Expose
    long offset;

    @Expose
    String uuid;
}