package com.aplusinformatique.aplusmanager.Services;

import android.Manifest;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.database.sqlite.SQLiteConstraintException;
import android.location.Location;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;

import com.aplusinformatique.aplusmanager.BuildConfig;
import com.aplusinformatique.aplusmanager.Classes.GeofenceBroadcastReceiver;
import com.aplusinformatique.aplusmanager.Classes.Singleton;
import com.aplusinformatique.aplusmanager.R;
import com.aplusinformatique.aplusmanager.api.ApiModule;
import com.aplusinformatique.aplusmanager.api.models.Directions;
import com.aplusinformatique.aplusmanager.api.models.Legs;
import com.aplusinformatique.aplusmanager.api.models.Routes;
import com.aplusinformatique.aplusmanager.db.AppDatabase;
import com.aplusinformatique.aplusmanager.db.Geofence;
import com.aplusinformatique.aplusmanager.db.GeofenceDao;
import com.aplusinformatique.aplusmanager.db.GeofenceEvent;
import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.GeofencingClient;
import com.google.android.gms.location.GeofencingRequest;
import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.location.LocationSettingsRequest;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

import static com.aplusinformatique.aplusmanager.db.Geofence.JSON_PARAM_GEOFENING_CHECK_DELAY;
import static com.aplusinformatique.aplusmanager.db.Geofence.JSON_PARAM_GEOFENING_ID;
import static com.aplusinformatique.aplusmanager.db.Geofence.JSON_PARAM_GEOFENING_LATITUDE;
import static com.aplusinformatique.aplusmanager.db.Geofence.JSON_PARAM_GEOFENING_LONGITUDE;
import static com.aplusinformatique.aplusmanager.db.Geofence.JSON_PARAM_GEOFENING_RADIUS;
import static com.aplusinformatique.aplusmanager.db.Geofence.JSON_PARAM_GEOFENING_ROUTING_API;
import static com.aplusinformatique.aplusmanager.db.Geofence.JSON_PARAM_GEOFENING_WARNING_DELAY;
import static com.google.android.gms.location.GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE;
import static com.google.android.gms.location.GeofenceStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES;
import static com.google.android.gms.location.GeofenceStatusCodes.GEOFENCE_TOO_MANY_PENDING_INTENTS;

public class DebutGeolocalisationV3 extends Service {

    // FIXME : TODO list
    // Cleanup the code.

    //Provides access to the Location Settings API.
//    private SettingsClient mSettingsClient;
    //Stores the types of location services the client is interested in using. Used for checking
    //settings to determine if the device has optimal location settings.
//    private LocationSettingsRequest mLocationSettingsRequest;
    private static final String TAG = "distrimobile_GeolocV3";

    //The desired interval for location updates. Inexact. Updates may be more or less frequent.
    private static final long UPDATE_INTERVAL_IN_MILLISECONDS = 10000;

    //The fastest rate for active location updates. Exact. Updates will never be more frequent
    //than this value.
    private static final long FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS = UPDATE_INTERVAL_IN_MILLISECONDS / 2;

    //Provides access to the Fused Location Provider API.
    private FusedLocationProviderClient mFusedLocationClient;
    //Stores parameters for requests to the FusedLocationProviderApi.
    private LocationRequest mLocationRequest;

    //Callback for Location events.
    private LocationCallback mLocationCallback;

    //Represents a geographical location.
    private Location mCurrentLocation;
    private Location mLastLocation = null;

    //Tracks the status of the location updates request. Value changes when the user presses the
    //Start Updates and Stop Updates buttons.
    private Boolean mRequestingLocationUpdates;

    //Time when the location was updated represented as a String.
    private String mLastUpdateTime;

    //Heartbeat
    private boolean declencherFinService = false;
    Handler h = new Handler();
    Runnable r;

    //Broadcast envoi géoposition
    public final static String ACTION_ENVOI_POSITION = "com.aplusinformatique.aplusmanager.geolocalisationreponsev3";

    //Broadcast receiver heartbeat
    public final static String ACTION_HEART_BEAT = "com.aplusinformatique.aplusmanager.heartbeat";
    public final static String ACTION_ACCUSE_HEART_BEAT = "com.aplusinformatique.aplusmanager.accuseheartbeat";
    private int accuseHeartBeat = 0;
    private BroadcastReceiver receiverAccuseReception;

    //Broadcast receiver Fin Service
    private final String actionFinService = "com.aplusinformatique.aplusmanager.fingeolocalisationv3";
    private BroadcastReceiver receiverFinService;

    //Broadcast receiver GeoFencing
    private final static String ACTION_GEOFENCING = "com.aplusinformatique.aplusmanager.geofencing";
    private final static String INTENT_PARAMETER_GEOFENCING_LIST = "geofences";
    private final static String JSON_GEOFENCING_LIST = "geofences";
    private BroadcastReceiver receiverGeoFencing;

    private final static String ACTION_GEOFENCE_TRIGGERED = "com.aplusinformatique.aplusmanager.geofencetriggered";
    private final static String INTENT_PARAMETER_TRIGGERED_GEOFENCE_ID = "geofence_id";
    private final static String INTENT_PARAMETER_TRIGGERED_GEOFENCE_ESTIMATED_DURATION = "estimated_duration";
    private final static String INTENT_PARAMETER_TRIGGERED_GEOFENCE_ESTIMATED_DISTANCE = "estimated_distance";
    private final static String INTENT_PARAMETER_TRIGGERED_GEOFENCE_TRANSITION = "geofence_transition";
    private BroadcastReceiver receiverGeoFencingTriggered;

    public final static String ACTION_ALARM_POSITION_TRIGGERED = "com.aplusinformatique.aplusmanager.alarmtriggered";
    public final static String INTENT_PARAMETER_ALARM_GEOFENCE_ID = "geofence_id";

    public final static String ACTION_API_ERROR_TRIGGERED = "com.aplusinformatique.aplusmanager.apierror";
    public final static String INTENT_PARAMETER_API_ERROR_GEOFENCE_ID = "geofence_id";
    public final static String INTENT_PARAMETER_API_ERROR_STATUS = "status";

    private final int ID_NOTIFICATION = 1002;
    private GeofencingClient geofencingClient;
    private HashMap<String, Geofence> geofenceListParameters = new HashMap<>();
    private ArrayList<com.google.android.gms.location.Geofence> mGeofenceList = new ArrayList<>();

    // Handle the case when the geofence is trigged AND the position has not been sent yet
    Vector<String> pendingTriggeredGeofenceId = new Vector<>();

    // Handle the Google API call error
    private final int GOOGLE_API_MAX_ERROR = 10;
    private int googleApiErrorCounter = 0;
    private boolean isGoogleAPIOK = true;


    /**
     * Used when requesting to add or remove geofences.
     */
    private PendingIntent mGeofencePendingIntent;
    private OnCompleteListener mOnCompleteListener = new OnCompleteListener<Void>() {
        @Override
        public void onComplete(@NonNull Task<Void> task) {
            if (task.isSuccessful()) {

//                                        int messageId = getGeofencesAdded() ? R.string.geofences_added :
//                                                R.string.geofences_removed;
//                                        Toast.makeText(this, getString(messageId), Toast.LENGTH_SHORT).show();
                if (BuildConfig.DEBUG || true) {
                    Log.d(TAG, "onComplete added geofences OK");
                }

            } else {
                // Get the status code for the error and log it using a user-friendly message.
//                                        String errorMessage = GeofenceErrorMessages.getErrorString(this, task.getException());
//                                        Log.w(TAG, errorMessage);
                // FIXME
                Log.w(TAG, task.getException().toString());
            }
        }
    };


    public static void startService(Context context) {
        if (BuildConfig.DEBUG || true) {
            Log.d(TAG, "startService ");
        }

        Intent intent = new Intent(context, DebutGeolocalisationV3.class);
        context.startService(intent);
    }

    public static void sendBroadcastHeartBeat(Context context) {
        Intent intent = new Intent();
        intent.setAction(ACTION_ACCUSE_HEART_BEAT);
        context.sendBroadcast(intent);
    }

    public static void sendGeoFenceBroadCast(Context context, List<Geofence> geofenceList) {
        if (BuildConfig.DEBUG || true) {
            Log.d(TAG, "sendGeoFenceBroadCast ");
        }

        try {
            // Put the list into JSON format
            JSONArray jsonArray = new JSONArray();
            if (geofenceList != null && geofenceList.size() > 0) {
                for (Geofence currentGeofence : geofenceList) {
                    jsonArray.put(currentGeofence.getJSONObject());
                }
            }
            JSONObject geofenceListObject = new JSONObject();
            geofenceListObject.put(JSON_GEOFENCING_LIST, jsonArray);

            // Send the list to the service
            Intent intent = new Intent();
            intent.setAction(ACTION_GEOFENCING);
            intent.putExtra(INTENT_PARAMETER_GEOFENCING_LIST, geofenceListObject.toString());
            context.sendBroadcast(intent);
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

    public static void sendApiError(Context context, String geofenceId, String status) {
        // Send the list to the service
        Intent intent = new Intent();
        intent.setAction(ACTION_API_ERROR_TRIGGERED);
        intent.putExtra(INTENT_PARAMETER_API_ERROR_GEOFENCE_ID, geofenceId);
        intent.putExtra(INTENT_PARAMETER_API_ERROR_STATUS, status);
        context.sendBroadcast(intent);
    }

    public static void sendGeofenceTriggered(Context context, int geofenceTransition, String geofenceId, long currentDuration, long currentDistance) {
        // Send the list to the service
        Intent intent = new Intent();
        intent.setAction(ACTION_GEOFENCE_TRIGGERED);
        intent.putExtra(INTENT_PARAMETER_TRIGGERED_GEOFENCE_TRANSITION, geofenceTransition);
        intent.putExtra(INTENT_PARAMETER_TRIGGERED_GEOFENCE_ID, geofenceId);
        intent.putExtra(INTENT_PARAMETER_TRIGGERED_GEOFENCE_ESTIMATED_DURATION, currentDuration);
        intent.putExtra(INTENT_PARAMETER_TRIGGERED_GEOFENCE_ESTIMATED_DISTANCE, currentDistance);

        context.sendBroadcast(intent);
    }

    public static void sendAlarmTriggered(Context context, String geofenceId, long currentDuration, long currentDistance) {
        // Send the list to the service
        Intent intent = new Intent();
        intent.setAction(ACTION_ALARM_POSITION_TRIGGERED);
        intent.putExtra(INTENT_PARAMETER_ALARM_GEOFENCE_ID, geofenceId);
        intent.putExtra(INTENT_PARAMETER_TRIGGERED_GEOFENCE_ESTIMATED_DURATION, currentDuration);
        intent.putExtra(INTENT_PARAMETER_TRIGGERED_GEOFENCE_ESTIMATED_DISTANCE, currentDistance);
        context.sendBroadcast(intent);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        super.onStartCommand(intent, flags, startId);
        Log.i(TAG, "onStartCommand DebutGeolocalisationV3 " + BuildConfig.VERSION_NAME);
        if (BuildConfig.DEBUG) {
            Log.i(TAG, "Version Debug");
        } else {
            Log.i(TAG, "Version Release : " + BuildConfig.FLAVOR);
        }

        NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        String channelId = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? createNotificationChannel(notificationManager) : "";
        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, channelId);
        Notification notification = notificationBuilder.setOngoing(true)
                .setSmallIcon(R.mipmap.ic_launcher)
                .setContentTitle("Géolocalisation V3")
                .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                .setCategory(NotificationCompat.CATEGORY_SERVICE)
                .build();

        startForeground(ID_NOTIFICATION, notification);

        IntentFilter filterFinService = new IntentFilter();
        filterFinService.addAction(actionFinService);

        receiverFinService = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                finService();
            }
        };
        registerReceiver(receiverFinService, filterFinService);

        mRequestingLocationUpdates = false;
        mLastUpdateTime = "";

        IntentFilter filter = new IntentFilter();
        filter.addAction(ACTION_ACCUSE_HEART_BEAT);

        receiverAccuseReception = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                accuseHeartBeat = 0;
            }
        };
        registerReceiver(receiverAccuseReception, filter);


        IntentFilter filterGeoFencing = new IntentFilter();
        filterGeoFencing.addAction(ACTION_GEOFENCING);
        receiverGeoFencing = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {

                String paramList = intent.getStringExtra(INTENT_PARAMETER_GEOFENCING_LIST);
                if (BuildConfig.DEBUG || true) {
                    Log.d(TAG, "onReceive ACTION_GEOFENCING JSON List " + paramList);
                }
                if (paramList != null && paramList.length() > 0) {
                    new HandleGeoFenceListTask().execute(paramList);
                } else {
                    Log.e(TAG, "onReceive ACTION_GEOFENCING JSON List IS NULL !!!");

                }
            }
        };
        registerReceiver(receiverGeoFencing, filterGeoFencing);


        IntentFilter filterGeoFencingTriggered = new IntentFilter();
        filterGeoFencingTriggered.addAction(ACTION_GEOFENCE_TRIGGERED);
        receiverGeoFencingTriggered = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {

                String paramGeofenceId = intent.getStringExtra(INTENT_PARAMETER_TRIGGERED_GEOFENCE_ID);
                int paramGeofenceTransition = intent.getIntExtra(INTENT_PARAMETER_TRIGGERED_GEOFENCE_TRANSITION, -1);
                if (BuildConfig.DEBUG || true) {
                    Log.d(TAG, "onReceive ACTION_GEOFENCE_TRIGGERED id = " + paramGeofenceId);
                    Log.d(TAG, "onReceive ACTION_GEOFENCE_TRIGGERED transition = " + paramGeofenceTransition);
                }

                // Remove the trigered geofence (to avoid double trig)
                // FIXME : removed to have all the events all the time : cancelGeofence(paramGeofenceId);

                // TODO : handle geofence exit ?
                new HandleGeofenceTriggeredTask().execute(new GeofenceEvent(paramGeofenceId, paramGeofenceTransition));
            }
        };
        registerReceiver(receiverGeoFencingTriggered, filterGeoFencingTriggered);


        mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
//        mSettingsClient = LocationServices.getSettingsClient(this);

        // Kick off the process of building the LocationCallback, LocationRequest, and
        // LocationSettingsRequest objects.
        createLocationCallback();

        createLocationRequest();

        buildLocationSettingsRequest();

        startLocationUpdates();

        //Si l'application Distrimobile ne réponds pas aux intent, on tue le service
        //Contrôle toutes les 5 secondes
        h.postDelayed(r = new

                Runnable() {
                    public void run() {

                        envoiHeartBeat();

                        if (mRequestingLocationUpdates == false) {
                            startLocationUpdates();
                        } else {
                            envoiPosition();
                        }

                        if (declencherFinService) {
                            finService();
                        } else {
                            h.postDelayed(this, 5 * 1000);
                        }
                    }
                }, 5 * 1000);


        geofencingClient = LocationServices.getGeofencingClient(this);

        return START_STICKY;
    }

    // TODO : Not used yet
    private void removeGeofenceByUid(String geofenceId) {
        // Delete the uid from the database
        new DeleteEntryTask(Singleton.getInstance().getDb(getApplicationContext()).geofenceDao()).execute(geofenceId);

        // Delete the uid from the list
        // FIXME : is it mandatory ?
        geofenceListParameters.remove(geofenceId);
        populateGeofenceList();

        // Remove the geofence from system
        cancelGeofence(geofenceId);
    }

    private class DeleteEntryTask extends AsyncTask<String, Void, Void> {

        private GeofenceDao mAsyncTaskDao;

        DeleteEntryTask(GeofenceDao dao) {
            mAsyncTaskDao = dao;
        }

        protected Void doInBackground(String... params) {
            String geofenceId = params[0];

            mAsyncTaskDao.deleteFromUid(geofenceId);
            return null;
        }
    }

    private void cancelGeofence(String geofenceId) {
        if (BuildConfig.DEBUG || true) {
            Log.d(TAG, "cancelGeofence try to cancel geofence " + geofenceId);
        }

        if (geofenceId != null && geofenceId.trim().length() > 0) {

            List<String> geofenceIdList = new ArrayList<>();
            geofenceIdList.add(geofenceId);

            geofencingClient.removeGeofences(geofenceIdList).addOnSuccessListener(new OnSuccessListener<Void>() {
                @Override
                public void onSuccess(Void aVoid) {
                    if (BuildConfig.DEBUG || true) {
                        Log.d(TAG, "onSuccess geofence" + geofenceId + " removed");
                    }
                }
            }).addOnFailureListener(new OnFailureListener() {
                @Override
                public void onFailure(@NonNull Exception e) {
                    if (BuildConfig.DEBUG || true) {
                        Log.e(TAG, "onFailure geofence" + geofenceId + " NOT removed");
                    }
                }
            });
        }
    }

    private void cancelAllGeofence() {
        geofencingClient.removeGeofences(getGeofencePendingIntent()).addOnSuccessListener(new OnSuccessListener<Void>() {
            @Override
            public void onSuccess(Void aVoid) {
                if (BuildConfig.DEBUG || true) {
                    Log.d(TAG, "onSuccess geofences removed");
                }
            }
        }).addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                if (BuildConfig.DEBUG || true) {
                    Log.e(TAG, "onFailure geofences NOT removed");
                }
            }
        });
    }

    /**
     * Sets up the location request. Android has two location request settings:
     * {@code ACCESS_COARSE_LOCATION} and {@code ACCESS_FINE_LOCATION}. These settings control
     * the accuracy of the current location. This sample uses ACCESS_FINE_LOCATION, as defined in
     * the AndroidManifest.xml.
     * <p/>
     * When the ACCESS_FINE_LOCATION setting is specified, combined with a fast update
     * interval (5 seconds), the Fused Location Provider API returns location updates that are
     * accurate to within a few feet.
     * <p/>
     * These settings are appropriate for mapping applications that show real-time location
     * updates.
     */
    private void createLocationRequest() {
        mLocationRequest = new LocationRequest();

        // Sets the desired interval for active location updates. This interval is
        // inexact. You may not receive updates at all if no location sources are available, or
        // you may receive them slower than requested. You may also receive updates faster than
        // requested if other applications are requesting location at a faster interval.
        mLocationRequest.setInterval(UPDATE_INTERVAL_IN_MILLISECONDS);

        // Sets the fastest rate for active location updates. This interval is exact, and your
        // application will never receive updates faster than this value.
        mLocationRequest.setFastestInterval(FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS);

        mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
    }

    /**
     * Creates a callback for receiving location events.
     */
    private void createLocationCallback() {
        mLocationCallback = new LocationCallback() {
            @Override
            public void onLocationResult(LocationResult locationResult) {
                super.onLocationResult(locationResult);

                mCurrentLocation = locationResult.getLastLocation();
                Date currentTime = Calendar.getInstance().getTime();
                SimpleDateFormat mySimpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
                mLastUpdateTime = mySimpleDateFormat.format(currentTime);
            }
        };
    }

    /**
     * Uses a {@link LocationSettingsRequest.Builder} to build
     * a {@link LocationSettingsRequest} that is used for checking
     * if a device has the needed location settings.
     */
    private void buildLocationSettingsRequest() {
        LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder();
        builder.addLocationRequest(mLocationRequest);
//        mLocationSettingsRequest = builder.build();
    }

    /**
     * Requests location updates from the FusedLocationApi. Note: we don't call this unless location
     * runtime permission has been granted.
     */
    private void startLocationUpdates() {

        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
            Log.i(TAG, "DebutGeolocalisationV3 Settings ok.");
            mFusedLocationClient.requestLocationUpdates(mLocationRequest, mLocationCallback, Looper.myLooper());
            mRequestingLocationUpdates = true;
        } else {
            Log.i(TAG, "DebutGeolocalisationV3 Settings invalide.");
            mRequestingLocationUpdates = false;
        }

    }

    /**
     * Removes location updates from the FusedLocationApi.
     */
    private void stopLocationUpdates() {
        if (!mRequestingLocationUpdates) {
            return;
        }

        // It is a good practice to remove location requests when the activity is in a paused or
        // stopped state. Doing so helps battery performance and is especially
        // recommended in applications that request frequent location updates.
        mFusedLocationClient.removeLocationUpdates(mLocationCallback);
        mRequestingLocationUpdates = false;
    }

    private void envoiPosition() {

        //Log.d(TAG, "envoiPosition DebutGeolocalisationV3");

        if (mCurrentLocation != null && mCurrentLocation.getLatitude() != 0 && mCurrentLocation.getLongitude() != 0) {

            if (mLastLocation == null || mLastLocation != mCurrentLocation) {

                mLastLocation = mCurrentLocation;

                Log.d(TAG, "position gps latitude : date : " + mLastUpdateTime + " " + mCurrentLocation.getLatitude() + " longitude : " + mCurrentLocation.getLongitude() + " precision : " + mCurrentLocation.getAccuracy() + " provider : " + mCurrentLocation.getProvider());

                Intent replyIntent = new Intent();
                replyIntent.setAction(ACTION_ENVOI_POSITION);
                replyIntent.putExtra("latitude", String.valueOf(mCurrentLocation.getLatitude()));
                replyIntent.putExtra("longitude", String.valueOf(mCurrentLocation.getLongitude()));
                replyIntent.putExtra("speed", String.valueOf(mCurrentLocation.getSpeed()));
                replyIntent.putExtra("accuracy", String.valueOf(mCurrentLocation.getAccuracy()));
                replyIntent.putExtra("date", mLastUpdateTime);
                sendBroadcast(replyIntent);

                // Handle the pending geofences already triggered
                if (!pendingTriggeredGeofenceId.isEmpty()) {
                    if (BuildConfig.DEBUG || true) {
                        Log.d(TAG, "envoiPosition handling pending geofences : " + pendingTriggeredGeofenceId.size());
                    }

                    // Handle concurrency
                    Vector<String> geofencesList = (Vector<String>) pendingTriggeredGeofenceId.clone();
                    for (String currentGeofenceId : geofencesList) {
                        if (BuildConfig.DEBUG || true) {
                            Log.d(TAG, "envoiPosition handling pending geofences id : " + currentGeofenceId);
                        }
                        // Remove the id from the original list
                        pendingTriggeredGeofenceId.remove(currentGeofenceId);
                        // FIXME : check the transition paramterer
                        new HandleGeofenceTriggeredTask().execute(new GeofenceEvent(currentGeofenceId, 0));
                    }
                } else {
                    if (BuildConfig.DEBUG || true) {
                        Log.d(TAG, "envoiPosition handling pending geofences");
                    }
                }

            }
        }
    }

    private void envoiHeartBeat() {

//        Log.d(TAG, "envoiHeartBeat DebutGeolocalisationV3");

        Intent replyIntent = new Intent();
        replyIntent.setAction(ACTION_HEART_BEAT);
        sendBroadcast(replyIntent);

        accuseHeartBeat += 1;
//        Log.d(TAG, "DebutGeolocalisationV3 compteur accusé heartbeat : " + String.valueOf(accuseHeartBeat));
        if (accuseHeartBeat > 4) {
            Log.w(TAG, "DebutGeolocalisationV3 accusés sans réponses");
            declencherFinService = true;
        }
    }

    // Geofencing
    private void populateGeofenceList() {
        mGeofenceList.clear();

        for (Map.Entry<String, Geofence> entry : geofenceListParameters.entrySet()) {

            mGeofenceList.add(new com.google.android.gms.location.Geofence.Builder()
                    // Set the request ID of the geofence. This is a string to identify this
                    // geofence.
                    .setRequestId(entry.getKey())

                    // Set the circular region of this geofence.
                    .setCircularRegion(
                            entry.getValue().latitude,
                            entry.getValue().longitude,
                            entry.getValue().radius
//                            43.291599,5.378226, 200
                    )

                    // Set the expiration duration of the geofence. This geofence gets automatically
                    // removed after this period of time.
                    .setExpirationDuration(entry.getValue().expirationDelay)

                    // Set the transition types of interest. Alerts are only generated for these
                    // transition. We track entry and exit transitions in this sample.
                    .setTransitionTypes(com.google.android.gms.location.Geofence.GEOFENCE_TRANSITION_ENTER | com.google.android.gms.location.Geofence.GEOFENCE_TRANSITION_EXIT)

                    // Create the geofence.
                    .build());
        }
    }

    // Geofencing
    private boolean checkPermissionsGeofencing() {
        int permissionState = ActivityCompat.checkSelfPermission(this,
                Manifest.permission.ACCESS_FINE_LOCATION);
        return permissionState == PackageManager.PERMISSION_GRANTED;
    }

    /**
     * Builds and returns a GeofencingRequest. Specifies the list of geofences to be monitored.
     * Also specifies how the geofence notifications are initially triggered.
     */
    private GeofencingRequest getGeofencingRequest() {
        if (BuildConfig.DEBUG || true) {
            Log.d(TAG, "getGeofencingRequest ");
        }

        GeofencingRequest.Builder builder = new GeofencingRequest.Builder();

        // The INITIAL_TRIGGER_ENTER flag indicates that geofencing service should trigger a
        // GEOFENCE_TRANSITION_ENTER notification when the geofence is added and if the device
        // is already inside that geofence.
        builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER);

        // Add the geofences to be monitored by geofencing service.
        builder.addGeofences(mGeofenceList);

        if (BuildConfig.DEBUG || true) {
            Log.d(TAG, "getGeofencingRequest " + mGeofenceList.size() + " " + mGeofenceList.toArray().toString());
        }


        // Return a GeofencingRequest.
        return builder.build();
    }

    /**
     * Gets a PendingIntent to send with the request to add or remove Geofences. Location Services
     * issues the Intent inside this PendingIntent whenever a geofence transition occurs for the
     * current list of geofences.
     *
     * @return A PendingIntent for the IntentService that handles geofence transitions.
     */
    private PendingIntent getGeofencePendingIntent() {
        if (BuildConfig.DEBUG || true) {
            Log.d(TAG, "getGeofencePendingIntent ");
        }


        // Reuse the PendingIntent if we already have it.
        if (mGeofencePendingIntent != null) {
            return mGeofencePendingIntent;
        }
        Intent intent = new Intent(this, GeofenceBroadcastReceiver.class);
        // We use FLAG_UPDATE_CURRENT so that we get the same pending intent back when calling
        // addGeofences() and removeGeofences().
        mGeofencePendingIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        return mGeofencePendingIntent;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        Log.d(TAG, "onDestroy DebutGeolocalisationV3");

        stopLocationUpdates();

        if (h != null) {
            h.removeCallbacks(r);
        }

        if (receiverAccuseReception != null) {
            unregisterReceiver(receiverAccuseReception);
            receiverAccuseReception = null;
        }

        if (receiverFinService != null) {
            unregisterReceiver(receiverFinService);
            receiverFinService = null;
        }

        if (receiverGeoFencing != null) {
            unregisterReceiver(receiverGeoFencing);
            receiverGeoFencing = null;
        }

        if (receiverGeoFencingTriggered != null) {
            unregisterReceiver(receiverGeoFencingTriggered);
            receiverGeoFencingTriggered = null;
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @RequiresApi(Build.VERSION_CODES.O)
    private String createNotificationChannel(NotificationManager notificationManager) {
        String channelId = TAG;
        String channelName = "Géolocalisation V3";
        NotificationChannel channel = new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_LOW);
        // omitted the LED color
        channel.setImportance(NotificationManager.IMPORTANCE_LOW);
        channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
        notificationManager.createNotificationChannel(channel);
        return channelId;
    }

    private void finService() {
        // Remove all the geofences if the service has gone.
        cancelAllGeofence();
        stopForeground(true);
        stopSelf();
    }

    // AsyncTask<Params, Progress, Result>
    private class HandleGeoFenceListTask extends AsyncTask<String, Void, String> {

        @SuppressWarnings("MissingPermission")
        protected String doInBackground(String... params) {
            String paramList = params[0];

            // Database
            AppDatabase db = Singleton.getInstance().getDb(getApplicationContext());

            geofenceListParameters.clear();
            db.geofenceDao().deleteAll();

            if (BuildConfig.DEBUG || true) {
                Log.d(TAG, "doInBackground deleted all geofences from database");
                Log.d(TAG, "doInBackground size of the geofences table : " + db.geofenceDao().getAll().size());
            }

            try {
                JSONObject jObject = new JSONObject(paramList);
                JSONArray jArray = jObject.getJSONArray(JSON_GEOFENCING_LIST);

                // If the list is empty, then clean all.
                if (jArray != null) {

                    for (int i = 0; i < jArray.length(); i++) {
                        try {
                            JSONObject currentGeofenceJSON = jArray.getJSONObject(i);
                            // Get the parameters value
                            String paramId = currentGeofenceJSON.getString(JSON_PARAM_GEOFENING_ID);
                            double paramLatitude = currentGeofenceJSON.getDouble(JSON_PARAM_GEOFENING_LATITUDE);
                            double paramLongitude = currentGeofenceJSON.getDouble(JSON_PARAM_GEOFENING_LONGITUDE);
                            float paramRadius = (float) currentGeofenceJSON.getDouble(JSON_PARAM_GEOFENING_RADIUS);
                            long paramWarningDelay = currentGeofenceJSON.getLong(JSON_PARAM_GEOFENING_WARNING_DELAY);
                            long paramCheckDelay = currentGeofenceJSON.getLong(JSON_PARAM_GEOFENING_CHECK_DELAY);
                            String routingApi = currentGeofenceJSON.getString(JSON_PARAM_GEOFENING_ROUTING_API);


                            if (BuildConfig.DEBUG || true) {
                                Log.d(TAG, "onReceive ACTION_GEOFENCING id " + paramId);
                                Log.d(TAG, "onReceive ACTION_GEOFENCING latitude " + paramLatitude);
                                Log.d(TAG, "onReceive ACTION_GEOFENCING longitude " + paramLongitude);
                                Log.d(TAG, "onReceive ACTION_GEOFENCING radius " + paramRadius);
                                Log.d(TAG, "onReceive ACTION_GEOFENCING warning_delay " + paramWarningDelay);
                                Log.d(TAG, "onReceive ACTION_GEOFENCING check_delay " + paramCheckDelay);
                                Log.d(TAG, "onReceive ACTION_GEOFENCING routing_api " + routingApi);
                            }

                            Geofence currentGeofence = new Geofence(paramId, paramLatitude, paramLongitude, paramRadius, paramWarningDelay, paramCheckDelay, paramCheckDelay, routingApi);
                            geofenceListParameters.put(paramId, currentGeofence);

                            db.geofenceDao().insert(currentGeofence);

                        } catch (JSONException e) {
                            // Oops
                            e.printStackTrace();

                            return "Erreur JSON : " + e.getMessage();
                        } catch (SQLiteConstraintException e) {
                            // Oops
                            e.printStackTrace();
                            return "Erreur SQL : " + e.getMessage();
                        }
                    }

                }
            } catch (JSONException e) {
                e.printStackTrace();
                return "Erreur JSON : " + e.getMessage();
            } catch (NullPointerException e) {
                e.printStackTrace();
                return "Erreur Null Pointer : " + e.getMessage();
            }

            populateGeofenceList();

            // Remove the previous geofences anytime.
            cancelAllGeofence();

            // If the list of the geofences is empty, then do not try to add them.
            if (mGeofenceList != null && mGeofenceList.size() > 0) {
                if (!checkPermissionsGeofencing()) {
                    Log.e(TAG, "checkPermissionsGeofencing FALSE !!!");
                } else {
                    geofencingClient.addGeofences(getGeofencingRequest(), getGeofencePendingIntent())
                            .addOnCompleteListener(mOnCompleteListener)
                            .addOnSuccessListener(new OnSuccessListener() {
                                @Override
                                public void onSuccess(Object o) {
                                    if (BuildConfig.DEBUG || true) {
                                        Log.d(TAG, "onSuccess geofences added");
                                    }
                                }
                            })
                            .addOnFailureListener(new OnFailureListener() {
                                @Override
                                public void onFailure(@NonNull Exception e) {
                                    if (BuildConfig.DEBUG || true) {
                                        Log.d(TAG, "onFailure Geofence" + e);
//                                        Toast.makeText(getApplicationContext(), "GeoFence FAIL", Toast.LENGTH_LONG).show();
                                    }
                                    e.printStackTrace();

                                    if (e instanceof com.google.android.gms.common.api.ApiException) {

                                        switch (((ApiException) e).getStatusCode()) {
                                            case GEOFENCE_NOT_AVAILABLE:
                                                Log.e(TAG, "onFailure GEOFENCE_NOT_AVAILABLE\nUse phone \"Settings | Security & location | Location | Mode\" - toggle between \"High accuracy, Battery saving or Device only\" until you get this prompt (the settings path will vary depending on android build):");
                                                break;
                                            case GEOFENCE_TOO_MANY_GEOFENCES:
                                                Log.e(TAG, "onFailure GEOFENCE_TOO_MANY_GEOFENCES\nMax geofence number : 100");
                                                break;
                                            case GEOFENCE_TOO_MANY_PENDING_INTENTS:
                                                Log.e(TAG, "onFailure GEOFENCE_TOO_MANY_PENDING_INTENTS\nMax pending intent : 5");
                                                break;
                                        }
                                    }
                                }
                            });
                }
            } else {
                if (BuildConfig.DEBUG || true) {
                    Log.d(TAG, "doInBackground No geofence to add");
                }
            }

            return null;
        }

//        protected void onPostExecute(String result) {
//            if (result != null) {
//                Toast.makeText(DebutGeolocalisationV3.this, result, Toast.LENGTH_SHORT).show();
//            }
//        }
    }

    // AsyncTask<Params, Progress, Result>
    private class HandleGeofenceTriggeredTask extends AsyncTask<GeofenceEvent, Void, Void> {
        protected Void doInBackground(GeofenceEvent... params) {
            if (params.length != 1) {
                Log.e(TAG, "HandleGeofenceTriggeredTask parameters must be 1, received list size : " + params.length);
                return null;
            }

            GeofenceEvent currentGeofenceEvent = params[0];

            // Database
            AppDatabase db = Singleton.getInstance().getDb(getApplicationContext());

            List<Geofence> foundGeofence = db.geofenceDao().getFromUid(currentGeofenceEvent.id);
            if (foundGeofence != null && foundGeofence.size() == 1) {
                if (BuildConfig.DEBUG || true) {
                    Log.d(TAG, "sendBroadcastGeofence found geofence " + foundGeofence.get(0));
                }
                com.aplusinformatique.aplusmanager.db.Geofence geofenceFromDatabase = foundGeofence.get(0);
                Log.d(TAG, "Current geofence in database : " + geofenceFromDatabase.toString());

                // If we are entering the Geofence, then launch the routing process
                if(currentGeofenceEvent.transition == com.google.android.gms.location.Geofence.GEOFENCE_TRANSITION_ENTER) {
                    //Routing choice
                    switch (geofenceFromDatabase.routingApi) {
                        case "GOOGLEMAPS":
                            handleGeofencesWithGoogleMaps(db, geofenceFromDatabase);
                            break;
                        case "MAPOTEMPO":
                            handleGeofencesWithMapotempo();
                            break;
                        default:
                            if (BuildConfig.DEBUG || true) {
                                Log.d(TAG, "No routing API mode, Trig ALARM for the MAIN APP");
                            }
                            // Update the state in the database
                            db.geofenceDao().delete(geofenceFromDatabase);

                            sendGeofenceTriggered(getApplicationContext(), 0, geofenceFromDatabase.uid, 0, 0);
                            sendAlarmTriggered(getApplicationContext(), geofenceFromDatabase.uid, 0, 0);
                    }
                } else { // If we are exiting to the geofence, then stop the routing
                    geofenceFromDatabase.hasToCalculateRouting = false;
                    db.geofenceDao().update(geofenceFromDatabase);
                    return null;
                }

            } else {
                //if (BuildConfig.DEBUG || true) {
                Log.d(TAG, "doInBackground clean triggered Geofence, not used anymore");
                //}
                cancelGeofence(currentGeofenceEvent.id);
            }
            return null;
        }

        protected void onPostExecute(Void result) {
            // TODO
        }

    }

    private void handleGeofencesWithGoogleMaps(AppDatabase db, Geofence geofenceFromDatabase) {
        if (BuildConfig.DEBUG) {
            Log.d(TAG, "handleGeofencesWithGoogleMaps");
        }

        // If the geofence have been triggered EXIT, then do not continue the loop.
        if(!geofenceFromDatabase.hasToCalculateRouting){
            if (BuildConfig.DEBUG) {
                Log.d(TAG, "handleGeofencesWithGoogleMaps Exit the loop.");
            }
            return;
        }

        if (mLastLocation != null) {
            if (BuildConfig.DEBUG || true) {
                Log.d(TAG, "handleGeofencesWithGoogleMaps position OK");
            }
            ApiModule.getInstance().apiService.getDirectionsBetween(mLastLocation.getLatitude() + "," + mLastLocation.getLongitude(),
                    geofenceFromDatabase.latitude + "," + geofenceFromDatabase.longitude,
                    getString(R.string.google_maps_key)
            ).enqueue(new Callback<Directions>() {
                @Override
                public void onResponse(Call<Directions> call, Response<Directions> response) {
                    if (response != null && response.body() != null) {
                        Directions responseDirections = response.body();
                        if (BuildConfig.DEBUG || true) {
                            Log.d(TAG, "onResponse " + responseDirections);
                        }

                        // Handle Status
                        if (!responseDirections.status.equals(Directions.STATUS_OK)) {
                            handleGoogleApiError(geofenceFromDatabase.uid, responseDirections.status);
                            // Update the state in the database
                            new UpdateGeofenceTriggeredTask(db.geofenceDao()).execute(geofenceFromDatabase.uid);
                            return;
                        }

                        if (responseDirections.routes != null && responseDirections.routes.size() > 0) {
                            Routes currentRoute = responseDirections.routes.get(0);
                            if (currentRoute != null && currentRoute.legs != null && currentRoute.legs.size() > 0) {
                                Legs currentLeg = currentRoute.legs.get(0);
                                if (currentLeg != null && currentLeg.duration != null) {
                                    googleApiErrorCounter = 0;

                                    long currentDuration = currentLeg.duration.value;
                                    long currentDistance = currentLeg.distance.value;
                                    if (BuildConfig.DEBUG || true) {
                                        Log.d(TAG, "onResponse travel estimated duration " + currentDuration);
                                        Log.d(TAG, "onResponse travel estimated distance " + currentDistance);
                                    }

                                    // The warning delay is OK => Send a Broadcast to the main application only one time
                                    if (!geofenceFromDatabase.hasBeenTriggered && geofenceFromDatabase.warningDelay > currentDuration) {
                                        if (BuildConfig.DEBUG || true) {
                                            Log.d(TAG, "Trig ALARM for the MAIN APP warningDelay:" + geofenceFromDatabase.warningDelay + "> currentDuration:" + currentDuration);
                                        }

                                        sendAlarmTriggered(getApplicationContext(), geofenceFromDatabase.uid, currentDuration, currentDistance);

                                        // Update the state in the database
                                        new UpdateGeofenceTriggeredTask(db.geofenceDao()).execute(geofenceFromDatabase.uid);
                                    }

                                    // Always send an update (till the geofence is deleted from the database.
                                    if (BuildConfig.DEBUG || true) {
                                        Log.d(TAG, "Trig RETRY cause warningDelay:" + geofenceFromDatabase.warningDelay + "< currentDuration:" + currentDuration);
                                    }

                                    final Handler handler = new Handler();
                                    handler.postDelayed(new Runnable() {
                                        @Override
                                        public void run() {
                                            //trigg again the geofence after check delay
                                            // TODO : Handle the value
                                            if (BuildConfig.DEBUG || true) {
                                                Log.d(TAG, "Timeout after  " + geofenceFromDatabase.checkDelay + " trig the geofence again");
                                            }

                                            sendGeofenceTriggered(getApplicationContext(), com.google.android.gms.location.Geofence.GEOFENCE_TRANSITION_ENTER, geofenceFromDatabase.uid, currentDuration, currentDistance);
                                        }
                                    }, geofenceFromDatabase.checkDelay * 1000);

                                } else {
                                    if (BuildConfig.DEBUG || true) {
                                        Log.d(TAG, "handleGeofencesWithGoogleMaps duration is empty");
                                    }
                                    handleGoogleApiError(geofenceFromDatabase.uid, "DURATION_EMPTY");
                                }
                            } else {
                                if (BuildConfig.DEBUG || true) {
                                    Log.d(TAG, "handleGeofencesWithGoogleMaps routes legs is empty");
                                }
                                handleGoogleApiError(geofenceFromDatabase.uid, "LEGS_EMPTY");
                            }
                        } else {
                            if (BuildConfig.DEBUG || true) {
                                Log.d(TAG, "handleGeofencesWithGoogleMaps routes list is empty");
                            }
                            handleGoogleApiError(geofenceFromDatabase.uid, "ROUTE_LIST_EMPTY");
                        }
                    } else {
                        if (BuildConfig.DEBUG || true) {
                            Log.d(TAG, "handleGeofencesWithGoogleMaps reponse is empty");
                        }
                        handleGoogleApiError(geofenceFromDatabase.uid, "RESPONSE_EMPTY");
                    }
                }

                @Override
                public void onFailure(Call<Directions> call, Throwable t) {
                    t.printStackTrace();
                }
            });
        } else {
            Log.e(TAG, "doInBackground mLastLocation IS NULL !!");

            if (BuildConfig.DEBUG || true) {
                Log.d(TAG, "doInBackground adding currentGeofenceId to pending list");
            }
            pendingTriggeredGeofenceId.add(geofenceFromDatabase.uid);
        }
    }

    private void handleGoogleApiError(String uid, String status) {
        //pendingTriggeredGeofenceId.add(uid);
        sendApiError(getApplicationContext(), uid, status);
    }

    private void handleGeofencesWithMapotempo() {
        if (BuildConfig.DEBUG) {
            Log.d(TAG, "handleGeofencesWithMapotempo");
        }
    }

    // AsyncTask<Params, Progress, Result>
    private class UpdateGeofenceTriggeredTask extends AsyncTask<String, Void, Void> {
        GeofenceDao geofenceDao;

        public UpdateGeofenceTriggeredTask(GeofenceDao geofenceDao) {
            this.geofenceDao = geofenceDao;
        }

        protected Void doInBackground(String... params) {
            String geofenceUid = params[0];
            List<Geofence> geofencesFound = geofenceDao.getFromUid(geofenceUid);
            if (geofencesFound != null && geofencesFound.size() == 1) {
                Geofence currentGeofence = geofencesFound.get(0);
                currentGeofence.hasBeenTriggered = true;
                geofenceDao.update(currentGeofence);
            }
            return null;
        }
    }
}
