/*
 * This is the source code of Telegram for Android v. 5.x.x.
 * It is licensed under GNU GPL v. 2 or later.
 * You should have received a copy of the license in this archive (see LICENSE).
 *
 * Copyright Nikolai Kudashov, 2013-2018.
 */

package org.telegram.messenger;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Build;
import android.os.Bundle;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.SparseIntArray;

import org.telegram.SQLite.SQLiteCursor;
import org.telegram.SQLite.SQLitePreparedStatement;
import org.telegram.tgnet.NativeByteBuffer;
import org.telegram.tgnet.TLObject;
import org.telegram.tgnet.TLRPC;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;

import androidx.collection.LongSparseArray;

@SuppressLint("MissingPermission")
public class LocationController extends BaseController implements NotificationCenter.NotificationCenterDelegate, ILocationServiceProvider.IAPIConnectionCallbacks, ILocationServiceProvider.IAPIOnConnectionFailedListener {

    private LongSparseArray<SharingLocationInfo> sharingLocationsMap = new LongSparseArray<>();
    private ArrayList<SharingLocationInfo> sharingLocations = new ArrayList<>();
    public LongSparseArray<ArrayList<TLRPC.Message>> locationsCache = new LongSparseArray<>();
    private LongSparseArray<Integer> lastReadLocationTime = new LongSparseArray<>();
    private LocationManager locationManager;
    private GpsLocationListener gpsLocationListener = new GpsLocationListener();
    private GpsLocationListener networkLocationListener = new GpsLocationListener();
    private GpsLocationListener passiveLocationListener = new GpsLocationListener();
    private FusedLocationListener fusedLocationListener = new FusedLocationListener();
    private Location lastKnownLocation;
    private long lastLocationSendTime;
    private boolean locationSentSinceLastMapUpdate = true;
    private long lastLocationStartTime;
    private boolean started;
    private boolean lastLocationByMaps;
    private SparseIntArray requests = new SparseIntArray();
    private LongSparseArray<Boolean> cacheRequests = new LongSparseArray<>();
    private long locationEndWatchTime;
    private boolean shareMyCurrentLocation;

    private boolean lookingForPeopleNearby;

    public ArrayList<SharingLocationInfo> sharingLocationsUI = new ArrayList<>();
    private LongSparseArray<SharingLocationInfo> sharingLocationsMapUI = new LongSparseArray<>();

    private Boolean servicesAvailable;
    private boolean wasConnectedToPlayServices;
    private ILocationServiceProvider.IMapApiClient apiClient;
    private final static int PLAY_SERVICES_RESOLUTION_REQUEST = 9000;
    private final static long UPDATE_INTERVAL = 1000, FASTEST_INTERVAL = 1000;
    private final static int BACKGROUD_UPDATE_TIME = 30 * 1000;
    private final static int LOCATION_ACQUIRE_TIME = 10 * 1000;
    private final static int FOREGROUND_UPDATE_TIME = 20 * 1000;
    private final static int WATCH_LOCATION_TIMEOUT = 65 * 1000;
    private final static int SEND_NEW_LOCATION_TIME = 2 * 1000;

    private ArrayList<TLRPC.TL_peerLocated> cachedNearbyUsers = new ArrayList<>();
    private ArrayList<TLRPC.TL_peerLocated> cachedNearbyChats = new ArrayList<>();

    private ILocationServiceProvider.ILocationRequest locationRequest;

    private static volatile LocationController[] Instance = new LocationController[UserConfig.MAX_ACCOUNT_COUNT];

    public static LocationController getInstance(int num) {
        LocationController localInstance = Instance[num];
        if (localInstance == null) {
            synchronized (LocationController.class) {
                localInstance = Instance[num];
                if (localInstance == null) {
                    Instance[num] = localInstance = new LocationController(num);
                }
            }
        }
        return localInstance;
    }

    public static class SharingLocationInfo {
        public long did;
        public int mid;
        public int stopTime;
        public int period;
        public int account;
        public int proximityMeters;
        public int lastSentProximityMeters;
        public MessageObject messageObject;
    }

    private class GpsLocationListener implements LocationListener {

        @Override
        public void onLocationChanged(Location location) {
            if (location == null) {
                return;
            }
            if (lastKnownLocation != null && (this == networkLocationListener || this == passiveLocationListener)) {
                if (!started && location.distanceTo(lastKnownLocation) > 20) {
                    setLastKnownLocation(location);
                    lastLocationSendTime = SystemClock.elapsedRealtime() - BACKGROUD_UPDATE_TIME + 5000;
                }
            } else {
                setLastKnownLocation(location);
            }
        }

        @Override
        public void onStatusChanged(String provider, int status, Bundle extras) {

        }

        @Override
        public void onProviderEnabled(String provider) {

        }

        @Override
        public void onProviderDisabled(String provider) {

        }
    }

    private class FusedLocationListener implements ILocationServiceProvider.ILocationListener {

        @Override
        public void onLocationChanged(Location location) {
            if (location == null) {
                return;
            }
            setLastKnownLocation(location);
        }
    }

    public LocationController(int instance) {
        super(instance);

        locationManager = (LocationManager) ApplicationLoader.applicationContext.getSystemService(Context.LOCATION_SERVICE);
        apiClient = ApplicationLoader.getLocationServiceProvider().onCreateLocationServicesAPI(ApplicationLoader.applicationContext, this, this);

        locationRequest = ApplicationLoader.getLocationServiceProvider().onCreateLocationRequest();
        locationRequest.setPriority(ILocationServiceProvider.PRIORITY_HIGH_ACCURACY);
        locationRequest.setInterval(UPDATE_INTERVAL);
        locationRequest.setFastestInterval(FASTEST_INTERVAL);

        AndroidUtilities.runOnUIThread(() -> {
            LocationController locationController = getAccountInstance().getLocationController();
            getNotificationCenter().addObserver(locationController, NotificationCenter.didReceiveNewMessages);
            getNotificationCenter().addObserver(locationController, NotificationCenter.messagesDeleted);
            getNotificationCenter().addObserver(locationController, NotificationCenter.replaceMessagesObjects);
        });
        loadSharingLocations();
    }

    @SuppressWarnings("unchecked")
    @Override
    public void didReceivedNotification(int id, int account, Object... args) {
        if (id == NotificationCenter.didReceiveNewMessages) {
            boolean scheduled = (Boolean) args[2];
            if (scheduled) {
                return;
            }
            long did = (Long) args[0];
            if (!isSharingLocation(did)) {
                return;
            }
            ArrayList<TLRPC.Message> messages = locationsCache.get(did);
            if (messages == null) {
                return;
            }
            ArrayList<MessageObject> arr = (ArrayList<MessageObject>) args[1];
            boolean added = false;
            for (int a = 0; a < arr.size(); a++) {
                MessageObject messageObject = arr.get(a);
                if (messageObject.isLiveLocation()) {
                    added = true;
                    boolean replaced = false;
                    for (int b = 0; b < messages.size(); b++) {
                        if (MessageObject.getFromChatId(messages.get(b)) == messageObject.getFromChatId()) {
                            replaced = true;
                            messages.set(b, messageObject.messageOwner);
                            break;
                        }
                    }
                    if (!replaced) {
                        messages.add(messageObject.messageOwner);
                    }
                } else if (messageObject.messageOwner.action instanceof TLRPC.TL_messageActionGeoProximityReached) {
                    long dialogId = messageObject.getDialogId();
                    if (DialogObject.isUserDialog(dialogId)) {
                        setProximityLocation(dialogId, 0, false);
                    }
                }
            }
            if (added) {
                NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.liveLocationsCacheChanged, did, currentAccount);
            }
        } else if (id == NotificationCenter.messagesDeleted) {
            boolean scheduled = (Boolean) args[2];
            if (scheduled) {
                return;
            }
            if (!sharingLocationsUI.isEmpty()) {
                ArrayList<Integer> markAsDeletedMessages = (ArrayList<Integer>) args[0];
                long channelId = (Long) args[1];
                ArrayList<Long> toRemove = null;
                for (int a = 0; a < sharingLocationsUI.size(); a++) {
                    SharingLocationInfo info = sharingLocationsUI.get(a);
                    long messageChannelId = info.messageObject != null ? info.messageObject.getChannelId() : 0;
                    if (channelId != messageChannelId) {
                        continue;
                    }
                    if (markAsDeletedMessages.contains(info.mid)) {
                        if (toRemove == null) {
                            toRemove = new ArrayList<>();
                        }
                        toRemove.add(info.did);
                    }
                }
                if (toRemove != null) {
                    for (int a = 0; a < toRemove.size(); a++) {
                        removeSharingLocation(toRemove.get(a));
                    }
                }
            }
        } else if (id == NotificationCenter.replaceMessagesObjects) {
            long did = (long) args[0];
            if (!isSharingLocation(did)) {
                return;
            }
            ArrayList<TLRPC.Message> messages = locationsCache.get(did);
            if (messages == null) {
                return;
            }
            boolean updated = false;
            ArrayList<MessageObject> messageObjects = (ArrayList<MessageObject>) args[1];
            for (int a = 0; a < messageObjects.size(); a++) {
                MessageObject messageObject = messageObjects.get(a);
                for (int b = 0; b < messages.size(); b++) {
                    if (MessageObject.getFromChatId(messages.get(b)) == messageObject.getFromChatId()) {
                        if (!messageObject.isLiveLocation()) {
                            messages.remove(b);
                        } else {
                            messages.set(b, messageObject.messageOwner);
                        }
                        updated = true;
                        break;
                    }
                }
            }
            if (updated) {
                NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.liveLocationsCacheChanged, did, currentAccount);
            }
        }
    }

    @Override
    public void onConnected(Bundle bundle) {
        wasConnectedToPlayServices = true;
        try {
            if (Build.VERSION.SDK_INT >= 21) {
                ApplicationLoader.getLocationServiceProvider().checkLocationSettings(locationRequest, status -> {
                    switch (status) {
                        case ILocationServiceProvider.STATUS_SUCCESS:
                            startFusedLocationRequest(true);
                            break;
                        case ILocationServiceProvider.STATUS_RESOLUTION_REQUIRED:
                            Utilities.stageQueue.postRunnable(() -> {
                                if (lookingForPeopleNearby || !sharingLocations.isEmpty()) {
                                    AndroidUtilities.runOnUIThread(() -> getNotificationCenter().postNotificationName(NotificationCenter.needShowPlayServicesAlert, status));
                                }
                            });
                            break;
                        case ILocationServiceProvider.STATUS_SETTINGS_CHANGE_UNAVAILABLE:
                            Utilities.stageQueue.postRunnable(() -> {
                                servicesAvailable = false;
                                try {
                                    apiClient.disconnect();
                                    start();
                                } catch (Throwable ignore) {}
                            });
                            break;
                    }
                });
            } else {
                startFusedLocationRequest(true);
            }
        } catch (Throwable e) {
            FileLog.e(e);
        }
    }

    public void startFusedLocationRequest(boolean permissionsGranted) {
        Utilities.stageQueue.postRunnable(() -> {
            if (!permissionsGranted) {
                servicesAvailable = false;
            }
            if (shareMyCurrentLocation || lookingForPeopleNearby || !sharingLocations.isEmpty()) {
                if (permissionsGranted) {
                    try {
                        ApplicationLoader.getLocationServiceProvider().getLastLocation(this::setLastKnownLocation);
                        ApplicationLoader.getLocationServiceProvider().requestLocationUpdates(locationRequest, fusedLocationListener);
                    } catch (Throwable e) {
                        FileLog.e(e);
                    }
                } else {
                    start();
                }
            }
        });
    }

    @Override
    public void onConnectionSuspended(int i) {

    }

    @Override
    public void onConnectionFailed() {
        if (wasConnectedToPlayServices) {
            return;
        }
        servicesAvailable = false;
        if (started) {
            started = false;
            start();
        }
    }

    private boolean checkServices() {
        if (servicesAvailable == null) {
            servicesAvailable = ApplicationLoader.getLocationServiceProvider().checkServices();
        }
        return servicesAvailable;
    }

    private void broadcastLastKnownLocation(boolean cancelCurrent) {
        if (lastKnownLocation == null) {
            return;
        }
        if (requests.size() != 0) {
            if (cancelCurrent) {
                for (int a = 0; a < requests.size(); a++) {
                    getConnectionsManager().cancelRequest(requests.keyAt(a), false);
                }
            }
            requests.clear();
        }
        if (!sharingLocations.isEmpty()) {
            int date = getConnectionsManager().getCurrentTime();
            float[] result = new float[1];
            for (int a = 0; a < sharingLocations.size(); a++) {
                final SharingLocationInfo info = sharingLocations.get(a);
                if (info.messageObject.messageOwner.media != null && info.messageObject.messageOwner.media.geo != null && info.lastSentProximityMeters == info.proximityMeters) {
                    int messageDate = info.messageObject.messageOwner.edit_date != 0 ? info.messageObject.messageOwner.edit_date : info.messageObject.messageOwner.date;
                    TLRPC.GeoPoint point = info.messageObject.messageOwner.media.geo;
                    if (Math.abs(date - messageDate) < 10) {
                        Location.distanceBetween(point.lat, point._long, lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude(), result);
                        if (result[0] < 1.0f) {
                            continue;
                        }
                    }
                }
                TLRPC.TL_messages_editMessage req = new TLRPC.TL_messages_editMessage();
                req.peer = getMessagesController().getInputPeer(info.did);
                req.id = info.mid;
                req.flags |= 16384;
                req.media = new TLRPC.TL_inputMediaGeoLive();
                req.media.stopped = false;
                req.media.geo_point = new TLRPC.TL_inputGeoPoint();
                req.media.geo_point.lat = AndroidUtilities.fixLocationCoord(lastKnownLocation.getLatitude());
                req.media.geo_point._long = AndroidUtilities.fixLocationCoord(lastKnownLocation.getLongitude());
                req.media.geo_point.accuracy_radius = (int) lastKnownLocation.getAccuracy();
                if (req.media.geo_point.accuracy_radius != 0) {
                    req.media.geo_point.flags |= 1;
                }
                if (info.lastSentProximityMeters != info.proximityMeters) {
                    req.media.proximity_notification_radius = info.proximityMeters;
                    req.media.flags |= 8;
                }
                req.media.heading = getHeading(lastKnownLocation);
                req.media.flags |= 4;
                final int[] reqId = new int[1];
                reqId[0] = getConnectionsManager().sendRequest(req, (response, error) -> {
                    if (error != null) {
                        if (error.text.equals("MESSAGE_ID_INVALID")) {
                            sharingLocations.remove(info);
                            sharingLocationsMap.remove(info.did);
                            saveSharingLocation(info, 1);
                            requests.delete(reqId[0]);
                            AndroidUtilities.runOnUIThread(() -> {
                                sharingLocationsUI.remove(info);
                                sharingLocationsMapUI.remove(info.did);
                                if (sharingLocationsUI.isEmpty()) {
                                    stopService();
                                }
                                NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.liveLocationsChanged);
                            });
                        }
                        return;
                    }
                    if ((req.flags & 8) != 0) {
                        info.lastSentProximityMeters = req.media.proximity_notification_radius;
                    }
                    TLRPC.Updates updates = (TLRPC.Updates) response;
                    boolean updated = false;
                    for (int a1 = 0; a1 < updates.updates.size(); a1++) {
                        TLRPC.Update update = updates.updates.get(a1);
                        if (update instanceof TLRPC.TL_updateEditMessage) {
                            updated = true;
                            info.messageObject.messageOwner = ((TLRPC.TL_updateEditMessage) update).message;
                        } else if (update instanceof TLRPC.TL_updateEditChannelMessage) {
                            updated = true;
                            info.messageObject.messageOwner = ((TLRPC.TL_updateEditChannelMessage) update).message;
                        }
                    }
                    if (updated) {
                        saveSharingLocation(info, 0);
                    }
                    getMessagesController().processUpdates(updates, false);
                });
                requests.put(reqId[0], 0);
            }
        }
        if (shareMyCurrentLocation) {
            UserConfig userConfig = getUserConfig();
            userConfig.lastMyLocationShareTime = (int) (System.currentTimeMillis() / 1000);
            userConfig.saveConfig(false);

            TLRPC.TL_contacts_getLocated req = new TLRPC.TL_contacts_getLocated();
            req.geo_point = new TLRPC.TL_inputGeoPoint();
            req.geo_point.lat = lastKnownLocation.getLatitude();
            req.geo_point._long = lastKnownLocation.getLongitude();
            req.background = true;
            getConnectionsManager().sendRequest(req, (response, error) -> {

            });
        }
        getConnectionsManager().resumeNetworkMaybe();
        if (shouldStopGps() || shareMyCurrentLocation) {
            shareMyCurrentLocation = false;
            stop(false);
        }
    }

    private boolean shouldStopGps() {
        return SystemClock.elapsedRealtime() > locationEndWatchTime;
    }

    protected void setNewLocationEndWatchTime() {
        if (sharingLocations.isEmpty()) {
            return;
        }
        locationEndWatchTime = SystemClock.elapsedRealtime() + WATCH_LOCATION_TIMEOUT;
        start();
    }

    protected void update() {
        UserConfig userConfig = getUserConfig();
        if (ApplicationLoader.isScreenOn && !ApplicationLoader.mainInterfacePaused && !shareMyCurrentLocation &&
                userConfig.isClientActivated() && userConfig.isConfigLoaded() && userConfig.sharingMyLocationUntil != 0 && Math.abs(System.currentTimeMillis() / 1000 - userConfig.lastMyLocationShareTime) >= 60 * 60) {
            shareMyCurrentLocation = true;
        }
        if (!sharingLocations.isEmpty()) {
            for (int a = 0; a < sharingLocations.size(); a++) {
                final SharingLocationInfo info = sharingLocations.get(a);
                int currentTime = getConnectionsManager().getCurrentTime();
                if (info.stopTime <= currentTime) {
                    sharingLocations.remove(a);
                    sharingLocationsMap.remove(info.did);
                    saveSharingLocation(info, 1);
                    AndroidUtilities.runOnUIThread(() -> {
                        sharingLocationsUI.remove(info);
                        sharingLocationsMapUI.remove(info.did);
                        if (sharingLocationsUI.isEmpty()) {
                            stopService();
                        }
                        NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.liveLocationsChanged);
                    });
                    a--;
                }
            }
        }
        if (started) {
            long newTime = SystemClock.elapsedRealtime();
            if (lastLocationByMaps || Math.abs(lastLocationStartTime - newTime) > LOCATION_ACQUIRE_TIME || shouldSendLocationNow()) {
                lastLocationByMaps = false;
                locationSentSinceLastMapUpdate = true;
                boolean cancelAll = (SystemClock.elapsedRealtime() - lastLocationSendTime) > 2 * 1000;
                lastLocationStartTime = newTime;
                lastLocationSendTime = SystemClock.elapsedRealtime();
                broadcastLastKnownLocation(cancelAll);
            }
        } else if (!sharingLocations.isEmpty() || shareMyCurrentLocation) {
            if (shareMyCurrentLocation || Math.abs(lastLocationSendTime - SystemClock.elapsedRealtime()) > BACKGROUD_UPDATE_TIME) {
                lastLocationStartTime = SystemClock.elapsedRealtime();
                start();
            }
        }
    }

    private boolean shouldSendLocationNow() {
        if (!shouldStopGps()) {
            return false;
        }
        if (Math.abs(lastLocationSendTime - SystemClock.elapsedRealtime()) >= SEND_NEW_LOCATION_TIME) {
            return true;
        }
        return false;
    }

    public void cleanup() {
        sharingLocationsUI.clear();
        sharingLocationsMapUI.clear();
        locationsCache.clear();
        cacheRequests.clear();
        cachedNearbyUsers.clear();
        cachedNearbyChats.clear();
        lastReadLocationTime.clear();
        stopService();
        Utilities.stageQueue.postRunnable(() -> {
            locationEndWatchTime = 0;
            requests.clear();
            sharingLocationsMap.clear();
            sharingLocations.clear();
            setLastKnownLocation(null);
            stop(true);
        });
    }

    private void setLastKnownLocation(Location location) {
        if (location != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && (SystemClock.elapsedRealtimeNanos() - location.getElapsedRealtimeNanos()) / 1000000000 > 60 * 5) {
            return;
        }
        lastKnownLocation = location;
        if (lastKnownLocation != null) {
            AndroidUtilities.runOnUIThread(() -> NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.newLocationAvailable));
        }
    }

    public void setCachedNearbyUsersAndChats(ArrayList<TLRPC.TL_peerLocated> u, ArrayList<TLRPC.TL_peerLocated> c) {
        cachedNearbyUsers = new ArrayList<>(u);
        cachedNearbyChats = new ArrayList<>(c);
    }

    public ArrayList<TLRPC.TL_peerLocated> getCachedNearbyUsers() {
        return cachedNearbyUsers;
    }

    public ArrayList<TLRPC.TL_peerLocated> getCachedNearbyChats() {
        return cachedNearbyChats;
    }

    protected void addSharingLocation(TLRPC.Message message) {
        final SharingLocationInfo info = new SharingLocationInfo();
        info.did = message.dialog_id;
        info.mid = message.id;
        info.period = message.media.period;
        info.lastSentProximityMeters = info.proximityMeters = message.media.proximity_notification_radius;
        info.account = currentAccount;
        info.messageObject = new MessageObject(currentAccount, message, false, false);
        info.stopTime = getConnectionsManager().getCurrentTime() + info.period;
        final SharingLocationInfo old = sharingLocationsMap.get(info.did);
        sharingLocationsMap.put(info.did, info);
        if (old != null) {
            sharingLocations.remove(old);
        }
        sharingLocations.add(info);
        saveSharingLocation(info, 0);
        lastLocationSendTime = SystemClock.elapsedRealtime() - BACKGROUD_UPDATE_TIME + 5000;
        AndroidUtilities.runOnUIThread(() -> {
            if (old != null) {
                sharingLocationsUI.remove(old);
            }
            sharingLocationsUI.add(info);
            sharingLocationsMapUI.put(info.did, info);
            startService();
            NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.liveLocationsChanged);
        });
    }

    public boolean isSharingLocation(long did) {
        return sharingLocationsMapUI.indexOfKey(did) >= 0;
    }

    public SharingLocationInfo getSharingLocationInfo(long did) {
        return sharingLocationsMapUI.get(did);
    }

    public boolean setProximityLocation(long did, int meters, boolean broadcast) {
        SharingLocationInfo info = sharingLocationsMapUI.get(did);
        if (info != null) {
            info.proximityMeters = meters;
        }
        getMessagesStorage().getStorageQueue().postRunnable(() -> {
            try {
                SQLitePreparedStatement state = getMessagesStorage().getDatabase().executeFast("UPDATE sharing_locations SET proximity = ? WHERE uid = ?");
                state.requery();
                state.bindInteger(1, meters);
                state.bindLong(2, did);
                state.step();
                state.dispose();
            } catch (Exception e) {
                FileLog.e(e);
            }
        });
        if (broadcast) {
            Utilities.stageQueue.postRunnable(() -> broadcastLastKnownLocation(true));
        }
        return info != null;
    }

    public static int getHeading(Location location) {
        float val = location.getBearing();
        if (val > 0 && val < 1.0f) {
            if (val < 0.5f) {
                return 360;
            } else {
                return 1;
            }
        }
        return (int) val;
    }

    private void loadSharingLocations() {
        getMessagesStorage().getStorageQueue().postRunnable(() -> {
            final ArrayList<SharingLocationInfo> result = new ArrayList<>();
            final ArrayList<TLRPC.User> users = new ArrayList<>();
            final ArrayList<TLRPC.Chat> chats = new ArrayList<>();
            try {
                ArrayList<Long> usersToLoad = new ArrayList<>();
                ArrayList<Long> chatsToLoad = new ArrayList<>();
                SQLiteCursor cursor = getMessagesStorage().getDatabase().queryFinalized("SELECT uid, mid, date, period, message, proximity FROM sharing_locations WHERE 1");
                while (cursor.next()) {
                    SharingLocationInfo info = new SharingLocationInfo();
                    info.did = cursor.longValue(0);
                    info.mid = cursor.intValue(1);
                    info.stopTime = cursor.intValue(2);
                    info.period = cursor.intValue(3);
                    info.proximityMeters = cursor.intValue(5);
                    info.account = currentAccount;
                    NativeByteBuffer data = cursor.byteBufferValue(4);
                    if (data != null) {
                        info.messageObject = new MessageObject(currentAccount, TLRPC.Message.TLdeserialize(data, data.readInt32(false), false), false, false);
                        MessagesStorage.addUsersAndChatsFromMessage(info.messageObject.messageOwner, usersToLoad, chatsToLoad, null);
                        data.reuse();
                    }
                    result.add(info);
                    if (DialogObject.isChatDialog(info.did)) {
                        if (!chatsToLoad.contains(-info.did)) {
                            chatsToLoad.add(-info.did);
                        }
                    } else if (DialogObject.isUserDialog(info.did)) {
                        if (!usersToLoad.contains(info.did)) {
                            usersToLoad.add(info.did);
                        }
                    }
                }
                cursor.dispose();
                if (!chatsToLoad.isEmpty()) {
                    getMessagesStorage().getChatsInternal(TextUtils.join(",", chatsToLoad), chats);
                }
                if (!usersToLoad.isEmpty()) {
                    getMessagesStorage().getUsersInternal(TextUtils.join(",", usersToLoad), users);
                }
            } catch (Exception e) {
                FileLog.e(e);
            }
            if (!result.isEmpty()) {
                AndroidUtilities.runOnUIThread(() -> {
                    getMessagesController().putUsers(users, true);
                    getMessagesController().putChats(chats, true);
                    Utilities.stageQueue.postRunnable(() -> {
                        sharingLocations.addAll(result);
                        for (int a = 0; a < sharingLocations.size(); a++) {
                            SharingLocationInfo info = sharingLocations.get(a);
                            sharingLocationsMap.put(info.did, info);
                        }
                        AndroidUtilities.runOnUIThread(() -> {
                            sharingLocationsUI.addAll(result);
                            for (int a = 0; a < result.size(); a++) {
                                SharingLocationInfo info = result.get(a);
                                sharingLocationsMapUI.put(info.did, info);
                            }
                            startService();
                            NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.liveLocationsChanged);
                        });
                    });
                });
            }
        });
    }

    private void saveSharingLocation(final SharingLocationInfo info, final int remove) {
        getMessagesStorage().getStorageQueue().postRunnable(() -> {
            try {
                if (remove == 2) {
                    getMessagesStorage().getDatabase().executeFast("DELETE FROM sharing_locations WHERE 1").stepThis().dispose();
                } else if (remove == 1) {
                    if (info == null) {
                        return;
                    }
                    getMessagesStorage().getDatabase().executeFast("DELETE FROM sharing_locations WHERE uid = " + info.did).stepThis().dispose();
                } else {
                    if (info == null) {
                        return;
                    }
                    SQLitePreparedStatement state = getMessagesStorage().getDatabase().executeFast("REPLACE INTO sharing_locations VALUES(?, ?, ?, ?, ?, ?)");
                    state.requery();

                    NativeByteBuffer data = new NativeByteBuffer(info.messageObject.messageOwner.getObjectSize());
                    info.messageObject.messageOwner.serializeToStream(data);

                    state.bindLong(1, info.did);
                    state.bindInteger(2, info.mid);
                    state.bindInteger(3, info.stopTime);
                    state.bindInteger(4, info.period);
                    state.bindByteBuffer(5, data);
                    state.bindInteger(6, info.proximityMeters);

                    state.step();
                    state.dispose();
                    data.reuse();
                }
            } catch (Exception e) {
                FileLog.e(e);
            }
        });
    }

    public void removeSharingLocation(final long did) {
        Utilities.stageQueue.postRunnable(() -> {
            final SharingLocationInfo info = sharingLocationsMap.get(did);
            sharingLocationsMap.remove(did);
            if (info != null) {
                TLRPC.TL_messages_editMessage req = new TLRPC.TL_messages_editMessage();
                req.peer = getMessagesController().getInputPeer(info.did);
                req.id = info.mid;
                req.flags |= 16384;
                req.media = new TLRPC.TL_inputMediaGeoLive();
                req.media.stopped = true;
                req.media.geo_point = new TLRPC.TL_inputGeoPointEmpty();
                getConnectionsManager().sendRequest(req, (response, error) -> {
                    if (error != null) {
                        return;
                    }
                    getMessagesController().processUpdates((TLRPC.Updates) response, false);
                });
                sharingLocations.remove(info);
                saveSharingLocation(info, 1);
                AndroidUtilities.runOnUIThread(() -> {
                    sharingLocationsUI.remove(info);
                    sharingLocationsMapUI.remove(info.did);
                    if (sharingLocationsUI.isEmpty()) {
                        stopService();
                    }
                    NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.liveLocationsChanged);
                });
                if (sharingLocations.isEmpty()) {
                    stop(true);
                }
            }
        });
    }

    private void startService() {
        if (BuildVars.IS_CHAT_AIR) return;
        try {
            /*if (Build.VERSION.SDK_INT >= 26) {
                ApplicationLoader.applicationContext.startForegroundService(new Intent(ApplicationLoader.applicationContext, LocationSharingService.class));
            } else {*/
                ApplicationLoader.applicationContext.startService(new Intent(ApplicationLoader.applicationContext, LocationSharingService.class));
            //}
        } catch (Throwable e) {
            FileLog.e(e);
        }
    }

    private void stopService() {
        if (BuildVars.IS_CHAT_AIR) return;
        ApplicationLoader.applicationContext.stopService(new Intent(ApplicationLoader.applicationContext, LocationSharingService.class));
    }

    public void removeAllLocationSharings() {
        Utilities.stageQueue.postRunnable(() -> {
            for (int a = 0; a < sharingLocations.size(); a++) {
                SharingLocationInfo info = sharingLocations.get(a);
                TLRPC.TL_messages_editMessage req = new TLRPC.TL_messages_editMessage();
                req.peer = getMessagesController().getInputPeer(info.did);
                req.id = info.mid;
                req.flags |= 16384;
                req.media = new TLRPC.TL_inputMediaGeoLive();
                req.media.stopped = true;
                req.media.geo_point = new TLRPC.TL_inputGeoPointEmpty();
                getConnectionsManager().sendRequest(req, (response, error) -> {
                    if (error != null) {
                        return;
                    }
                    getMessagesController().processUpdates((TLRPC.Updates) response, false);
                });
            }
            sharingLocations.clear();
            sharingLocationsMap.clear();
            saveSharingLocation(null, 2);
            stop(true);
            AndroidUtilities.runOnUIThread(() -> {
                sharingLocationsUI.clear();
                sharingLocationsMapUI.clear();
                stopService();
                NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.liveLocationsChanged);
            });
        });
    }

    public void setMapLocation(Location location, boolean first) {
        if (location == null) {
            return;
        }
        lastLocationByMaps = true;
        if (first || lastKnownLocation != null && lastKnownLocation.distanceTo(location) >= 20) {
            lastLocationSendTime = SystemClock.elapsedRealtime() - BACKGROUD_UPDATE_TIME;
            locationSentSinceLastMapUpdate = false;
        } else if (locationSentSinceLastMapUpdate) {
            lastLocationSendTime = SystemClock.elapsedRealtime() - BACKGROUD_UPDATE_TIME + FOREGROUND_UPDATE_TIME;
            locationSentSinceLastMapUpdate = false;
        }
        setLastKnownLocation(location);
    }

    private void start() {
        if (started) {
            return;
        }
        lastLocationStartTime = SystemClock.elapsedRealtime();
        started = true;
        boolean ok = false;
        if (checkServices()) {
            try {
                apiClient.connect();
                ok = true;
            } catch (Throwable e) {
                FileLog.e(e);
            }
        }
        if (!ok) {
            try {
                locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1, 0, gpsLocationListener);
            } catch (Exception e) {
                FileLog.e(e);
            }
            try {
                locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 1, 0, networkLocationListener);
            } catch (Exception e) {
                FileLog.e(e);
            }
            try {
                locationManager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, 1, 0, passiveLocationListener);
            } catch (Exception e) {
                FileLog.e(e);
            }
            if (lastKnownLocation == null) {
                try {
                    setLastKnownLocation(locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER));
                    if (lastKnownLocation == null) {
                        setLastKnownLocation(locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER));
                    }
                } catch (Exception e) {
                    FileLog.e(e);
                }
            }
        }
    }

    private void stop(boolean empty) {
        if (lookingForPeopleNearby || shareMyCurrentLocation) {
            return;
        }
        started = false;
        if (checkServices()) {
            try {
                ApplicationLoader.getLocationServiceProvider().removeLocationUpdates(fusedLocationListener);
                apiClient.disconnect();
            } catch (Throwable e) {
                FileLog.e(e, false);
            }
        }
        locationManager.removeUpdates(gpsLocationListener);
        if (empty) {
            locationManager.removeUpdates(networkLocationListener);
            locationManager.removeUpdates(passiveLocationListener);
        }
    }

    public void startLocationLookupForPeopleNearby(boolean stop) {
        Utilities.stageQueue.postRunnable(() -> {
            lookingForPeopleNearby = !stop;
            if (lookingForPeopleNearby) {
                start();
            } else if (sharingLocations.isEmpty()) {
                stop(true);
            }
        });
    }

    public Location getLastKnownLocation() {
        return lastKnownLocation;
    }

    public void loadLiveLocations(long did) {
        if (cacheRequests.indexOfKey(did) >= 0) {
            return;
        }
        cacheRequests.put(did, true);
        TLRPC.TL_messages_getRecentLocations req = new TLRPC.TL_messages_getRecentLocations();
        req.peer = getMessagesController().getInputPeer(did);
        req.limit = 100;
        getConnectionsManager().sendRequest(req, (response, error) -> {
            if (error != null) {
                return;
            }
            AndroidUtilities.runOnUIThread(() -> {
                cacheRequests.delete(did);
                TLRPC.messages_Messages res = (TLRPC.messages_Messages) response;
                for (int a = 0; a < res.messages.size(); a++) {
                    if (!(res.messages.get(a).media instanceof TLRPC.TL_messageMediaGeoLive)) {
                        res.messages.remove(a);
                        a--;
                    }
                }
                getMessagesStorage().putUsersAndChats(res.users, res.chats, true, true);
                getMessagesController().putUsers(res.users, false);
                getMessagesController().putChats(res.chats, false);
                locationsCache.put(did, res.messages);
                NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.liveLocationsCacheChanged, did, currentAccount);
            });
        });
    }

    public void markLiveLoactionsAsRead(long dialogId) {
        if (DialogObject.isEncryptedDialog(dialogId)) {
            return;
        }
        ArrayList<TLRPC.Message> messages = locationsCache.get(dialogId);
        if (messages == null || messages.isEmpty()) {
            return;
        }
        Integer date = lastReadLocationTime.get(dialogId);
        int currentDate = (int) (SystemClock.elapsedRealtime() / 1000);
        if (date != null && date + 60 > currentDate) {
            return;
        }
        lastReadLocationTime.put(dialogId, currentDate);
        TLObject request;
        if (DialogObject.isChatDialog(dialogId) && ChatObject.isChannel(-dialogId, currentAccount)) {
            TLRPC.TL_channels_readMessageContents req = new TLRPC.TL_channels_readMessageContents();
            for (int a = 0, N = messages.size(); a < N; a++) {
                req.id.add(messages.get(a).id);
            }
            req.channel = getMessagesController().getInputChannel(-dialogId);
            request = req;
        } else {
            TLRPC.TL_messages_readMessageContents req = new TLRPC.TL_messages_readMessageContents();
            for (int a = 0, N = messages.size(); a < N; a++) {
                req.id.add(messages.get(a).id);
            }
            request = req;
        }
        getConnectionsManager().sendRequest(request, (response, error) -> {
            if (response instanceof TLRPC.TL_messages_affectedMessages) {
                TLRPC.TL_messages_affectedMessages res = (TLRPC.TL_messages_affectedMessages) response;
                getMessagesController().processNewDifferenceParams(-1, res.pts, -1, res.pts_count);
            }
        });
    }

    public static int getLocationsCount() {
        int count = 0;
        for (int a = 0; a < UserConfig.MAX_ACCOUNT_COUNT; a++) {
            count += LocationController.getInstance(a).sharingLocationsUI.size();
        }
        return count;
    }

    public interface LocationFetchCallback {
        void onLocationAddressAvailable(String address, String displayAddress, Location location);
    }

    private static HashMap<LocationFetchCallback, Runnable> callbacks = new HashMap<>();
    public static void fetchLocationAddress(Location location, LocationFetchCallback callback) {
        if (callback == null) {
            return;
        }
        Runnable fetchLocationRunnable = callbacks.get(callback);
        if (fetchLocationRunnable != null) {
            Utilities.globalQueue.cancelRunnable(fetchLocationRunnable);
            callbacks.remove(callback);
        }
        if (location == null) {
            callback.onLocationAddressAvailable(null, null, null);
            return;
        }

        Utilities.globalQueue.postRunnable(fetchLocationRunnable = () -> {
            String name;
            String displayName;
            try {
                Geocoder gcd = new Geocoder(ApplicationLoader.applicationContext, LocaleController.getInstance().getSystemDefaultLocale());
                List<Address> addresses = gcd.getFromLocation(location.getLatitude(), location.getLongitude(), 1);
                if (addresses.size() > 0) {
                    Address address = addresses.get(0);
                    boolean hasAny = false;
                    String arg;

                    StringBuilder nameBuilder = new StringBuilder();
                    StringBuilder displayNameBuilder = new StringBuilder();

                    arg = address.getSubThoroughfare();
                    if (!TextUtils.isEmpty(arg)) {
                        nameBuilder.append(arg);
                        hasAny = true;
                    }
                    arg = address.getThoroughfare();
                    if (!TextUtils.isEmpty(arg)) {
                        if (nameBuilder.length() > 0) {
                            nameBuilder.append(" ");
                        }
                        nameBuilder.append(arg);
                        hasAny = true;
                    }
                    if (!hasAny) {
                        arg = address.getAdminArea();
                        if (!TextUtils.isEmpty(arg)) {
                            if (nameBuilder.length() > 0) {
                                nameBuilder.append(", ");
                            }
                            nameBuilder.append(arg);
                        }
                        arg = address.getSubAdminArea();
                        if (!TextUtils.isEmpty(arg)) {
                            if (nameBuilder.length() > 0) {
                                nameBuilder.append(", ");
                            }
                            nameBuilder.append(arg);
                        }
                    }
                    arg = address.getLocality();
                    if (!TextUtils.isEmpty(arg)) {
                        if (nameBuilder.length() > 0) {
                            nameBuilder.append(", ");
                        }
                        nameBuilder.append(arg);
                    }
                    arg = address.getCountryName();
                    if (!TextUtils.isEmpty(arg)) {
                        if (nameBuilder.length() > 0) {
                            nameBuilder.append(", ");
                        }
                        nameBuilder.append(arg);
                    }

                    arg = address.getCountryName();
                    if (!TextUtils.isEmpty(arg)) {
                        if (displayNameBuilder.length() > 0) {
                            displayNameBuilder.append(", ");
                        }
                        displayNameBuilder.append(arg);
                    }
                    arg = address.getLocality();
                    if (!TextUtils.isEmpty(arg)) {
                        if (displayNameBuilder.length() > 0) {
                            displayNameBuilder.append(", ");
                        }
                        displayNameBuilder.append(arg);
                    }
                    if (!hasAny) {
                        arg = address.getAdminArea();
                        if (!TextUtils.isEmpty(arg)) {
                            if (displayNameBuilder.length() > 0) {
                                displayNameBuilder.append(", ");
                            }
                            displayNameBuilder.append(arg);
                        }
                        arg = address.getSubAdminArea();
                        if (!TextUtils.isEmpty(arg)) {
                            if (displayNameBuilder.length() > 0) {
                                displayNameBuilder.append(", ");
                            }
                            displayNameBuilder.append(arg);
                        }
                    }

                    name = nameBuilder.toString();
                    displayName = displayNameBuilder.toString();
                } else {
                    name = displayName = String.format(Locale.US, "Unknown address (%f,%f)", location.getLatitude(), location.getLongitude());
                }
            } catch (Exception ignore) {
                name = displayName = String.format(Locale.US, "Unknown address (%f,%f)", location.getLatitude(), location.getLongitude());
            }
            final String nameFinal = name;
            final String displayNameFinal = displayName;
            AndroidUtilities.runOnUIThread(() -> {
                callbacks.remove(callback);
                callback.onLocationAddressAvailable(nameFinal, displayNameFinal, location);
            });
        }, 300);
        callbacks.put(callback, fetchLocationRunnable);
    }
}
