package de.dennisguse.opentracks.services.handlers;

import android.content.Context;
import android.location.LocationManager;
import android.os.Handler;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import java.time.Duration;
import java.time.Instant;

import de.dennisguse.opentracks.data.models.Distance;
import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.settings.PreferencesUtils;

/**
 * This class handle GPS status according to received locations` and some thresholds.
 */
class GpsStatusManager {

    private static final String TAG = GpsStatusManager.class.getSimpleName();

    // The duration that GpsStatus waits from minimal interval to consider GPS lost.
    @VisibleForTesting
    public static final Duration SIGNAL_LOST_THRESHOLD = Duration.ofSeconds(30);

    private Distance horizontalAccuracyThreshold;
    // Threshold for time without points.
    private Duration signalLostThreshold;

    private GpsStatusValue gpsStatus = GpsStatusValue.GPS_NONE;
    private GpsStatusListener client;
    private final Context context;

    @Nullable
    private TrackPoint lastTrackPoint = null;

    // Flag to prevent GpsStatus checks two or more locations at the same time.
    private boolean checking = false;

    private Handler handler;

    public final Runnable gpsStatusTimer = () -> {
        determineGpsStatusByTime(Instant.now()); //TODO Get now via TrackPointCreator?
    };


    public GpsStatusManager(Context context, GpsStatusListener client, Handler handler) {
        this.client = client;
        this.context = context;
        this.handler = handler;

        onRecordingDistanceChanged(PreferencesUtils.getRecordingDistanceInterval());
        onMinRecordingIntervalChanged(PreferencesUtils.getMinRecordingInterval());
    }

    public void start() {
        client.onGpsStatusChanged(GpsStatusValue.GPS_ENABLED);
    }

    /**
     * The client that uses GpsStatus has to call this method to stop the Runnable if needed.
     */
    public void stop() {
        client.onGpsStatusChanged(GpsStatusValue.GPS_NONE);
        client = null;
        handler = null;
    }

    /**
     * Method to change the bad threshold from outside.
     *
     * @param value New preference value to signalBadThreshold.
     */
    public void onRecordingDistanceChanged(@NonNull Distance value) {
        horizontalAccuracyThreshold = value;
    }

    public void onMinRecordingIntervalChanged(Duration value) {
        signalLostThreshold = SIGNAL_LOST_THRESHOLD.plus(value); //TODO Reschedule gpsStatusTimer?
    }

    /**
     * This method must be called from the client every time a new trackPoint is received.
     * Receive new trackPoint and calculate the new status if needed.
     * It look for GPS changes in lastLocation if it's not null. If it's null then look for in lastValidLocation if any.
     */
    //TODO Remove checking; should be synchronized if this is a problem.
    public void onNewTrackPoint(@NonNull final TrackPoint trackPoint) {
        if (checking) {
            return;
        }

        checking = true;
        lastTrackPoint = trackPoint;

        determineGpsStatusOnTrackpoint(trackPoint);

        checking = false;
    }

    /**
     * Checks if lastLocation has new GPS status looking up time and accuracy.
     * It depends of signalLostThreshold and signalBadThreshold.
     * If there is any change then it does the change.
     * Also, it'll run the runnable if signal is bad or stop it if the signal is lost.
     */
    //TODO use MonotonicClock instead of Instant.now()
    @VisibleForTesting
    void determineGpsStatusOnTrackpoint(@NonNull TrackPoint lastTrackPoint) {
        if (lastTrackPoint.fulfillsAccuracy(horizontalAccuracyThreshold)) {
            if (gpsStatus != GpsStatusValue.GPS_SIGNAL_FIX) {
                setGpsStatus(GpsStatusValue.GPS_SIGNAL_FIX);
                scheduleTimer(); //TODO
            }
        } else {
            // GPS signal is to weak; TODO we might need a time-based threshold here as well (i.e., warn after Duration)
            if (gpsStatus != GpsStatusValue.GPS_SIGNAL_BAD) {
                setGpsStatus(GpsStatusValue.GPS_SIGNAL_BAD);
                scheduleTimer();
            }
        }
    }

    void determineGpsStatusByTime(Instant now) {
        if (lastTrackPoint == null) {
            return;
        }
        if (signalLostThreshold.minus(Duration.between(lastTrackPoint.getTime(), now)).isNegative()) {
            // Too much time without receiving signal -> signal lost.
            if (gpsStatus != GpsStatusValue.GPS_SIGNAL_LOST) {
                setGpsStatus(GpsStatusValue.GPS_SIGNAL_LOST);
            }
            return;
        }
        scheduleTimer();
    }

    /**
     * This method must be called from the client every time the GPS sensor is enabled.
     * Anyway, it checks that GPS is enabled because the client assumes that if it's on then GPS is enabled but user can disable GPS by hand.
     */
    public void onGpsEnabled() {
        if (gpsStatus == GpsStatusValue.GPS_ENABLED) {
            return;
        }
        LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
        if (locationManager != null && locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
            setGpsStatus(GpsStatusValue.GPS_ENABLED);
            scheduleTimer();
        } else {
            onGpsDisabled();
        }
    }


    /**
     * This method must be called from service every time the GPS sensor is disabled.
     */
    public void onGpsDisabled() {
        if (gpsStatus == GpsStatusValue.GPS_DISABLED) {
            return;
        }

        setGpsStatus(GpsStatusValue.GPS_DISABLED);
        lastTrackPoint = null;
        stopTimer();
    }

    private void setGpsStatus(GpsStatusValue current) {
        gpsStatus = current;
        if (client != null) {
            client.onGpsStatusChanged(current);
        }
    }

    private void scheduleTimer() {
        handler.removeCallbacks(gpsStatusTimer);
        handler.postDelayed(gpsStatusTimer, signalLostThreshold.toMillis());
    }

    private void stopTimer() {
        handler.removeCallbacks(gpsStatusTimer);
    }

    public interface GpsStatusListener {
        void onGpsStatusChanged(GpsStatusValue currentStatus);
    }
}
