package com.nexenio.bleindoorpositioningdemo.location;

import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.common.api.CommonStatusCodes;
import com.google.android.gms.common.api.ResolvableApiException;
import com.google.android.gms.location.FusedLocationProviderClient;
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.location.LocationSettingsResponse;
import com.google.android.gms.location.LocationSettingsStatusCodes;
import com.google.android.gms.location.SettingsClient;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.PackageManager;
import android.os.Build;
import android.provider.Settings;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import android.text.TextUtils;
import android.util.Log;

import com.nexenio.bleindoorpositioning.location.Location;
import com.nexenio.bleindoorpositioning.location.LocationListener;
import com.nexenio.bleindoorpositioning.location.provider.LocationProvider;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * Created by steppschuh on 21.11.17.
 */

public final class AndroidLocationProvider implements LocationProvider {

    private static final String TAG = AndroidLocationProvider.class.getSimpleName();
    public static final int REQUEST_CODE_LOCATION_PERMISSIONS = 1;
    public static final int REQUEST_CODE_LOCATION_SETTINGS = 2;

    private static AndroidLocationProvider instance;
    private Activity activity;
    private boolean isRequestingLocationUpdates;
    private LocationRequest locationRequest;
    private LocationCallback locationCallback;
    private FusedLocationProviderClient fusedLocationClient;

    private Location lastKnownLocation;
    private Set<LocationListener> locationListeners = new HashSet<>();

    private AndroidLocationProvider() {

    }

    public static AndroidLocationProvider getInstance() {
        if (instance == null) {
            instance = new AndroidLocationProvider();
        }
        return instance;
    }

    public static void initialize(@NonNull Activity activity) {
        Log.v(TAG, "Initializing with context: " + activity);
        AndroidLocationProvider instance = getInstance();
        instance.activity = activity;
        instance.fusedLocationClient = LocationServices.getFusedLocationProviderClient(activity);
        instance.setupLocationService();
    }

    private void setupLocationService() {
        Log.v(TAG, "Setting up location service");
        LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder()
                .addLocationRequest(getLocationRequest());
        SettingsClient client = LocationServices.getSettingsClient(activity);
        Task<LocationSettingsResponse> task = client.checkLocationSettings(builder.build());

        task.addOnSuccessListener(activity, new OnSuccessListener<LocationSettingsResponse>() {
            @Override
            public void onSuccess(LocationSettingsResponse locationSettingsResponse) {
                Log.v(TAG, "Location settings satisfied");
            }
        });

        task.addOnFailureListener(activity, new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                int statusCode = ((ApiException) e).getStatusCode();
                switch (statusCode) {
                    case CommonStatusCodes.RESOLUTION_REQUIRED:
                        Log.w(TAG, "Location settings not satisfied, attempting resolution intent");
                        try {
                            ResolvableApiException resolvable = (ResolvableApiException) e;
                            resolvable.startResolutionForResult(activity, REQUEST_CODE_LOCATION_SETTINGS);
                        } catch (IntentSender.SendIntentException sendIntentException) {
                            Log.e(TAG, "Unable to start resolution intent");
                        }
                        break;
                    case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE:
                        Log.w(TAG, "Location settings not satisfied and can't be changed");
                        break;
                }
            }
        });
    }

    public static boolean registerLocationListener(@NonNull LocationListener locationListener) {
        AndroidLocationProvider instance = getInstance();
        boolean added = instance.locationListeners.add(locationListener);
        if (added && !instance.isRequestingLocationUpdates) {
            startRequestingLocationUpdates();
        }
        return added;
    }

    public static boolean unregisterLocationListener(@NonNull LocationListener locationListener) {
        AndroidLocationProvider instance = getInstance();
        boolean removed = instance.locationListeners.remove(locationListener);
        if (removed && instance.isRequestingLocationUpdates && instance.locationListeners.isEmpty()) {
            stopRequestingLocationUpdates();
        }
        return removed;
    }

    @SuppressLint("MissingPermission")
    public static void startRequestingLocationUpdates() {
        AndroidLocationProvider instance = getInstance();
        if (instance.isRequestingLocationUpdates) {
            return;
        }
        if (!instance.hasLocationPermission()) {
            return;
        }
        Log.d(TAG, "Starting to request location updates");
        if (instance.locationListeners.isEmpty()) {
            Log.w(TAG, "There are no location listeners registered to process location updates");
        }
        instance.fusedLocationClient.requestLocationUpdates(instance.getLocationRequest(), instance.getLocationCallback(), null);
        instance.isRequestingLocationUpdates = true;
    }

    public static void stopRequestingLocationUpdates() {
        AndroidLocationProvider instance = getInstance();
        if (!instance.isRequestingLocationUpdates) {
            return;
        }
        Log.d(TAG, "Stopping to request location updates");
        if (!instance.locationListeners.isEmpty()) {
            Log.w(TAG, "There are still registered location listeners which won't receive new location updates");
        }
        instance.fusedLocationClient.removeLocationUpdates(instance.getLocationCallback());
        instance.isRequestingLocationUpdates = false;
    }

    @SuppressLint("MissingPermission")
    public static void requestLastKnownLocation() {
        final AndroidLocationProvider instance = getInstance();
        if (!instance.hasLocationPermission()) {
            return;
        }
        Log.d(TAG, "Requesting last known location");
        instance.fusedLocationClient.getLastLocation()
                .addOnSuccessListener(getInstance().activity, new OnSuccessListener<android.location.Location>() {
                    @Override
                    public void onSuccess(android.location.Location androidLocation) {
                        if (androidLocation != null) {
                            instance.onLocationUpdateReceived(androidLocation);
                        } else {
                            Log.w(TAG, "Unable to get last known location");
                        }
                    }
                });
    }

    private void onLocationUpdateReceived(@NonNull android.location.Location androidLocation) {
        Location location = convertLocation(androidLocation);
        onLocationUpdateReceived(location);
    }

    private void onLocationUpdateReceived(@NonNull Location location) {
        lastKnownLocation = location;
        Log.v(TAG, "Last known location set to: " + lastKnownLocation);
        for (LocationListener locationListener : locationListeners) {
            locationListener.onLocationUpdated(this, lastKnownLocation);
        }
    }

    private LocationRequest createHighAccuracyLocationRequest() {
        LocationRequest locationRequest = new LocationRequest();
        locationRequest.setInterval(TimeUnit.SECONDS.toMillis(3));
        locationRequest.setFastestInterval(TimeUnit.SECONDS.toMillis(1));
        locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
        return locationRequest;
    }

    private LocationRequest getLocationRequest() {
        if (locationRequest == null) {
            locationRequest = createHighAccuracyLocationRequest();
        }
        return locationRequest;
    }

    private LocationCallback getLocationCallback() {
        if (locationCallback == null) {
            locationCallback = createLocationCallback();
        }
        return locationCallback;
    }

    private LocationCallback createLocationCallback() {
        return new LocationCallback() {
            @Override
            public void onLocationResult(LocationResult locationResult) {
                Log.v(TAG, "Received location result with " + locationResult.getLocations().size() + " locations");
                onLocationUpdateReceived(locationResult.getLastLocation());
            }
        };
    }

    public boolean hasLocationPermission() {
        return hasLocationPermission(activity);
    }

    public static boolean hasLocationPermission(@NonNull Context context) {
        boolean fineLocation = ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
        boolean coarseLocation = ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
        return fineLocation && coarseLocation;
    }

    public static void requestLocationPermission(@NonNull Activity activity) {
        Log.d(TAG, "Requesting location permission");
        ActivityCompat.requestPermissions(activity, new String[]{
                Manifest.permission.ACCESS_FINE_LOCATION,
                Manifest.permission.ACCESS_COARSE_LOCATION
        }, REQUEST_CODE_LOCATION_PERMISSIONS);
    }

    public static boolean isLocationEnabled(@NonNull Context context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            try {
                int locationMode = Settings.Secure.getInt(context.getContentResolver(), Settings.Secure.LOCATION_MODE);
                return locationMode != Settings.Secure.LOCATION_MODE_OFF;
            } catch (Settings.SettingNotFoundException e) {
                Log.e(TAG, "Unable to get location mode");
                e.printStackTrace();
                return false;
            }
        } else {
            String locationProviders = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.LOCATION_PROVIDERS_ALLOWED);
            return !TextUtils.isEmpty(locationProviders);
        }
    }

    public static void requestLocationEnabling(@NonNull Activity activity) {
        Log.d(TAG, "Requesting location enabling");
        Intent locationSettings = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
        activity.startActivity(locationSettings);
    }

    public static Location convertLocation(android.location.Location androidLocation) {
        Location convertedLocation = new Location();
        convertedLocation.setLatitude(androidLocation.getLatitude());
        convertedLocation.setLongitude(androidLocation.getLongitude());
        convertedLocation.setAltitude(convertedLocation.getAltitude());
        convertedLocation.setElevation(convertedLocation.getElevation());
        return convertedLocation;
    }

    @Override
    public Location getLocation() {
        return lastKnownLocation;
    }

}