package com.darryncampbell.wifi_rtt_trilateration;

import android.annotation.SuppressLint;
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.net.wifi.ScanResult;
import android.net.wifi.WifiManager;
import android.net.wifi.rtt.RangingRequest;
import android.net.wifi.rtt.RangingResult;
import android.net.wifi.rtt.RangingResultCallback;
import android.net.wifi.rtt.ResponderLocation;
import android.net.wifi.rtt.WifiRttManager;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.util.Log;

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

import com.lemmingapex.trilateration.NonLinearLeastSquaresSolver;
import com.lemmingapex.trilateration.TrilaterationFunction;

import org.apache.commons.math3.fitting.leastsquares.LeastSquaresOptimizer;
import org.apache.commons.math3.fitting.leastsquares.LevenbergMarquardtOptimizer;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;

public class LocationRangingService extends Service {

    boolean mStarted = false;
    private static final String TAG = "Wifi-RTT";
    private WifiManager mWifiManager;
    private WifiScanReceiver mWifiScanReceiver;
    private List<ScanResult> WifiRttAPs;
    private WifiRttManager mWifiRttManager;
    private RttRangingResultCallback mRttRangingResultCallback;
    final Handler mRangeRequestDelayHandler = new Handler();
    private int mMillisecondsDelayBeforeNewRangingRequest = Configuration.MILLISECONDS_BETWEEN_RANGING_REQUESTS;
    private boolean bStop = true;
    private Configuration configuration;
    private List<AccessPoint> buildingMap;
    private HashMap<String, ArrayList<RangingResult>> historicalDistances;


    public LocationRangingService() {
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mWifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
        mWifiScanReceiver = new WifiScanReceiver();
        mWifiRttManager = (WifiRttManager) getSystemService(Context.WIFI_RTT_RANGING_SERVICE);
        mRttRangingResultCallback = new RttRangingResultCallback();
        configuration = new Configuration(Configuration.CONFIGURATION_TYPE.TESTING_3);
        //configuration = new Configuration(Configuration.CONFIGURATION_TYPE.TWO_DIMENSIONAL_2);
        buildingMap = configuration.getConfiguration();
        Collections.sort(buildingMap);
        historicalDistances = new HashMap<String, ArrayList<RangingResult>>();
        for (int i = 0; i < buildingMap.size(); i++)
        {
            historicalDistances.put(buildingMap.get(i).getBssid().toString(), new ArrayList<RangingResult>());
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId)
    {
        if (intent.getAction().equals(Constants.ACTION.START_LOCATION_RANGING_SERVICE))
        {
            if (mStarted)
            {

            }
            else
            {
                mStarted = true;
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    NotificationChannel notificationChannel = new NotificationChannel(Constants.NOTIFICATION_ID.LOCATION_UPDATE_CHANNEL_ID,
                            Constants.NOTIFICATION_ID.LOCATION_UPDATE_CHANNEL, NotificationManager.IMPORTANCE_HIGH);
                    NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
                    notificationManager.createNotificationChannel(notificationChannel);
                }
                Intent startRangingIntent = new Intent(LocationRangingService.this, LocationRangingService.class);
                startRangingIntent.setAction(Constants.ACTION.START_LOCATION_RANGING);
                Intent stopRangingIntent = new Intent(LocationRangingService.this, LocationRangingService.class);
                stopRangingIntent.setAction(Constants.ACTION.STOP_LOCATION_RANGING);
                Intent quitRangingIntent = new Intent(LocationRangingService.this, LocationRangingService.class);
                quitRangingIntent.setAction(Constants.ACTION.STOP_LOCATION_RANGING_SERVICE);

                Notification notification = new NotificationCompat.Builder(this, Constants.NOTIFICATION_ID.LOCATION_UPDATE_CHANNEL_ID)
                        .setContentTitle("WiFi RTT Location Ranging")
                        .setContentText("Location Ranging Service")
                        .setSmallIcon(R.drawable.ic_pin)
                        .setChannelId(Constants.NOTIFICATION_ID.LOCATION_UPDATE_CHANNEL_ID)
                        //.setContentIntent(pendingIntent)
                        .setOngoing(true)
                        .addAction(R.drawable.ic_pin, "Start Ranging",
                                PendingIntent.getForegroundService(this, Constants.NOTIFICATION_ID.LOCATION_RANGING_SERVICE, startRangingIntent, 0))
                        .addAction(R.drawable.ic_pin, "Stop Ranging",
                                PendingIntent.getForegroundService(this, Constants.NOTIFICATION_ID.LOCATION_RANGING_SERVICE, stopRangingIntent, 0))
                        .addAction(R.drawable.ic_pin, "Quit",
                                PendingIntent.getForegroundService(this, Constants.NOTIFICATION_ID.LOCATION_RANGING_SERVICE, quitRangingIntent, 0))
                                        .build();
                //.addAction(R.drawable.ic_pin_drop,
                //        "Record Location", pRecordLocationIntent).build();
                startForeground(Constants.NOTIFICATION_ID.LOCATION_RANGING_SERVICE,
                        notification);
            }


        }
        else if (intent.getAction().equals(Constants.ACTION.START_LOCATION_RANGING))
        {
            registerReceiver(
                    mWifiScanReceiver, new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION));

            if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI_RTT))
            {
                showMessage("This device does not support WIFI RTT");
            }
            else
            {
                //  startScan() is marked as deprecated but no alternative API is available (yet)
                bStop = false;
                showMessage("Searching for access points");
                mWifiManager.startScan();
            }

        }
        else if (intent.getAction().equals(Constants.ACTION.STOP_LOCATION_RANGING))
        {
            if (bStop)
                showMessage("Ranging is already stopped");
            else
                showMessage("Stopping ranging...");
            bStop = true;
        }
        else if (intent.getAction().equals(Constants.ACTION.STOP_LOCATION_RANGING_SERVICE))
        {
            mStarted = false;
            bStop = true;
            stopForeground(true);
            stopSelf();

            Intent messageIntent = new Intent(Constants.SERVICE_COMMS.FINISH);
            sendBroadcast(messageIntent);
        }

        return START_REDELIVER_INTENT;
    }

    @Override
    public IBinder onBind(Intent intent) {
        //  Used only for bound services
        return null;
    }

    private class WifiScanReceiver extends BroadcastReceiver {

        @SuppressLint("MissingPermission")
        public void onReceive(Context context, Intent intent) {
            List<ScanResult> scanResults = mWifiManager.getScanResults();
            if (scanResults != null) {

                WifiRttAPs = new ArrayList<>();
                for (ScanResult scanResult : scanResults) {
                    if (scanResult.is80211mcResponder())
                        WifiRttAPs.add(scanResult);
                    if (WifiRttAPs.size() >= RangingRequest.getMaxPeers()) {
                        break;
                    }
                }

                showMessage(scanResults.size()
                        + " APs discovered, "
                        + WifiRttAPs.size()
                        + " RTT capable.");
                for (ScanResult wifiRttAP : WifiRttAPs) {
                    showMessage("AP Supporting RTT: " + wifiRttAP.SSID + " (" + wifiRttAP.BSSID + ")");
                }
                //  Start ranging
                if (WifiRttAPs.size() < configuration.getConfiguration().size())
                    showMessage("Did not find enough RTT enabled APs.  Found: " + WifiRttAPs.size());
                else {
                    startRangingRequest(WifiRttAPs);
                }
            }
        }
    }

    @SuppressLint("MissingPermission")
    private void startRangingRequest(List<ScanResult> scanResults) {
        RangingRequest rangingRequest =
                new RangingRequest.Builder().addAccessPoints(scanResults).build();

        mWifiRttManager.startRanging(
                rangingRequest, getApplication().getMainExecutor(), mRttRangingResultCallback);
    }

    // Class that handles callbacks for all RangingRequests and issues new RangingRequests.
    private class RttRangingResultCallback extends RangingResultCallback {

        private void queueNextRangingRequest() {
            mRangeRequestDelayHandler.postDelayed(
                    new Runnable() {
                        @Override
                        public void run() {
                            startRangingRequest(WifiRttAPs);
                        }
                    },
                    mMillisecondsDelayBeforeNewRangingRequest);
        }

        @Override
        public void onRangingFailure(int code) {
            Log.d(TAG, "onRangingFailure() code: " + code);
            queueNextRangingRequest();
        }

        @Override
        public void onRangingResults(@NonNull List<RangingResult> rangingResultsList) {
            Log.d(TAG, "onRangingResults(): " + rangingResultsList);

            //  Ensure we have more APs in the list of ranging results than were present in the configuration
            if (rangingResultsList.size() >= configuration.getConfiguration().size()) {

                //  Sort the received ranging results by MAC address
                //  (order needs to match the order in the config, previously sorted by MAC address)
                Collections.sort(rangingResultsList, new Comparator<RangingResult>() {
                    @Override
                    public int compare(RangingResult o1, RangingResult o2) {
                        return o1.getMacAddress().toString().compareTo(o2.getMacAddress().toString());
                    }
                });

                //  Check that the received ranging results are valid and appropriate
                List<RangingResult> rangingResultsOfInterest = new ArrayList<>();
                rangingResultsOfInterest.clear();
                for (int i = 0; i < rangingResultsList.size(); i++) {
                    RangingResult rangingResult = rangingResultsList.get(i);
                    if (!configuration.getMacAddresses().contains(rangingResult.getMacAddress().toString())) {
                        //  The Mac address found is not in our configuration
                        showMessage("Unrecognised MAC address: " + rangingResult.getMacAddress().toString() + ", ignoring");
                    } else {
                        if (rangingResult.getStatus() == RangingResult.STATUS_SUCCESS) {
                            rangingResultsOfInterest.add(rangingResult);
                            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
                                ResponderLocation responderLocation = rangingResultsList.get(0).getUnverifiedResponderLocation();
                                if (responderLocation == null)
                                    Log.d(TAG, "ResponderLocation is null (not supported)");
                                else
                                    Log.d(TAG, "ResponderLocation is " + responderLocation.toString());
                            }
                        } else if (rangingResult.getStatus() == RangingResult.STATUS_RESPONDER_DOES_NOT_SUPPORT_IEEE80211MC) {
                            showMessage("RangingResult failed (AP doesn't support IEEE80211 MC.");
                        } else {
                            showMessage("RangingResult failed. (" + rangingResult.getMacAddress().toString() + ")");
                        }
                    }
                }
                //  rangingResultsOfInterest now contains the list of APs from whom we have received valid ranging results

                //  Check that every AP in our configuration returned a valid ranging result
                //  Potential enhancement: could remove any APs from the building map that we couldn't range to (need at least 2)
                if (rangingResultsOfInterest.size() != configuration.getConfiguration().size())
                {
                    showMessage("Could not find all the APs defined in the configuration to range off of");
                    if (!bStop)
                        queueNextRangingRequest();
                    return;
                }

                for (int i = 0; i < rangingResultsOfInterest.size(); i++)
                {
                    ArrayList temp = historicalDistances.get(rangingResultsOfInterest.get(i).getMacAddress().toString());
                    temp.add(rangingResultsOfInterest.get(i));
                    if (temp.size() == Configuration.NUM_HISTORICAL_POINTS + 1)
                        temp.remove(0);
                    showMessage("Distance to " + rangingResultsOfInterest.get(i).getMacAddress().toString() +
                            " [Ave]: " + (int)weighted_average(historicalDistances.get(rangingResultsOfInterest.get(i).getMacAddress().toString())) + "mm");
                    showMessage("Distance to " + rangingResultsOfInterest.get(i).getMacAddress().toString() +
                            " : " + rangingResultsOfInterest.get(i).getMacAddress().toString() + "mm");
                }

                //  historicalDistances now contains an arraylist of historic distances for each AP
                //  because of an earlier check, we know that every AP in the building map has an associated
                //  entry in the history of observed ranging results
                //  Create the positions and distances arrays required by the multilateration algorithm
                double[][] positions = new double[buildingMap.size()][3]; //  3 dimensions
                double[] distances = new double[buildingMap.size()];
                for (int i = 0; i < buildingMap.size(); i++)
                {
                    positions[i] = buildingMap.get(i).getPosition();
                    distances[i] = weighted_average(historicalDistances.get(rangingResultsOfInterest.get(i).getMacAddress().toString()));
                }

                try {
                    NonLinearLeastSquaresSolver solver = new NonLinearLeastSquaresSolver(new TrilaterationFunction(positions, distances), new LevenbergMarquardtOptimizer());
                    LeastSquaresOptimizer.Optimum optimum = solver.solve();
                    double[] centroid = optimum.getPoint().toArray();
                    Intent centroidIntent = new Intent(Constants.SERVICE_COMMS.LOCATION_COORDS);
                    centroidIntent.putExtra(Constants.SERVICE_COMMS.LOCATION_COORDS, centroid);
                    sendBroadcast(centroidIntent);
                }
                catch (Exception e)
                {
                    showMessage("Error during trilateration: " + e.getMessage());
                }

            }
            else
            {
                showMessage("Could not find enough Ranging Results");
            }
            if (!bStop)
                queueNextRangingRequest();
        }
    }


    private double weighted_average(List<RangingResult> rangingResults) {
        //  https://en.wikipedia.org/wiki/Weighted_arithmetic_mean#Variance_weights
        double weighted_numerator = 0.0;
        double weighted_demoninator = 0.0;
        for (int i = 0; i < rangingResults.size(); i++)
        {
            weighted_numerator += (rangingResults.get(i).getDistanceMm() * (1.0 / (rangingResults.get(i).getDistanceStdDevMm() ^ 2)));
            weighted_demoninator += (1.0 / (rangingResults.get(i).getDistanceStdDevMm() ^ 2));
        }
        return weighted_numerator / weighted_demoninator;
    }

    public void showMessage(String message) {
        Log.i(TAG, message);
        //txtDebugOutput.setText(message + '\n' + txtDebugOutput.getText());
        Intent messageIntent = new Intent(Constants.SERVICE_COMMS.MESSAGE);
        messageIntent.putExtra(Constants.SERVICE_COMMS.MESSAGE, message);
        sendBroadcast(messageIntent);
    }

}