package com.mapbox.services.android.navigation.v5.location.replay;

import android.annotation.SuppressLint;
import android.location.Location;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import com.mapbox.android.core.location.LocationEngine;
import com.mapbox.android.core.location.LocationEngineListener;
import com.mapbox.api.directions.v5.models.DirectionsRoute;
import com.mapbox.geojson.LineString;
import com.mapbox.geojson.Point;

import java.util.ArrayList;
import java.util.List;


public class ReplayRouteLocationEngine extends LocationEngine implements Runnable {

  private static final int HEAD = 0;
  private static final int MOCKED_POINTS_LEFT_THRESHOLD = 5;
  private static final int ONE_SECOND_IN_MILLISECONDS = 1000;
  private static final int FORTY_FIVE_KM_PER_HOUR = 45;
  private static final int DEFAULT_SPEED = FORTY_FIVE_KM_PER_HOUR;
  private static final int ONE_SECOND = 1;
  private static final int DEFAULT_DELAY = ONE_SECOND;
  private static final int DO_NOT_DELAY = 0;
  private static final int ZERO = 0;
  private static final String SPEED_MUST_BE_GREATER_THAN_ZERO_KM_H = "Speed must be greater than 0 km/h.";
  private static final String DELAY_MUST_BE_GREATER_THAN_ZERO_SECONDS = "Delay must be greater than 0 seconds.";
  private static final String REPLAY_ROUTE = "ReplayRouteLocation";
  private ReplayRouteLocationConverter converter;
  private int speed = DEFAULT_SPEED;
  private int delay = DEFAULT_DELAY;
  private Handler handler;
  private List<Location> mockedLocations;
  private ReplayLocationDispatcher dispatcher;
  private Location lastLocation = null;
  private final ReplayLocationListener replayLocationListener = new ReplayLocationListener() {
    @Override
    public void onLocationReplay(Location location) {
      for (LocationEngineListener listener : locationListeners) {
        listener.onLocationChanged(location);
      }
      lastLocation = location;
      if (!mockedLocations.isEmpty()) {
        mockedLocations.remove(HEAD);
      }
    }
  };

  public ReplayRouteLocationEngine() {
    this.handler = new Handler();
  }

  @SuppressLint("MissingPermission")
  public void assign(DirectionsRoute route) {
    start(route);
  }

  @SuppressLint("MissingPermission")
  public void moveTo(Point point) {
    Location lastLocation = getLastLocation();
    if (lastLocation == null) {
      return;
    }

    startRoute(point, lastLocation);
  }

  public void assignLastLocation(Point currentPosition) {
    initializeLastLocation();
    lastLocation.setLongitude(currentPosition.longitude());
    lastLocation.setLatitude(currentPosition.latitude());
  }

  public void updateSpeed(int customSpeedInKmPerHour) {
    if (customSpeedInKmPerHour <= 0) {
      throw new IllegalArgumentException(SPEED_MUST_BE_GREATER_THAN_ZERO_KM_H);
    }
    this.speed = customSpeedInKmPerHour;
  }

  public void updateDelay(int customDelayInSeconds) {
    if (customDelayInSeconds <= 0) {
      throw new IllegalArgumentException(DELAY_MUST_BE_GREATER_THAN_ZERO_SECONDS);
    }
    this.delay = customDelayInSeconds;
  }

  @Override
  public void run() {
    List<Location> nextMockedLocations = converter.toLocations();
    if (nextMockedLocations.isEmpty()) {
      handler.removeCallbacks(this);
      return;
    }
    dispatcher.add(nextMockedLocations);
    mockedLocations.addAll(nextMockedLocations);
    scheduleNextDispatch();
  }

  /**
   * Connect all the location listeners.
   */
  @Override
  public void activate() {
    for (LocationEngineListener listener : locationListeners) {
      listener.onConnected();
    }
  }

  @Override
  public void deactivate() {
    if (dispatcher != null) {
      dispatcher.stop();
    }
    handler.removeCallbacks(this);
  }

  /**
   * While the {@link ReplayRouteLocationEngine} is in use, you are always connected to it.
   *
   * @return true.
   */
  @Override
  public boolean isConnected() {
    return true;
  }

  @SuppressLint("MissingPermission")
  @Override
  @Nullable
  public Location getLastLocation() {
    return lastLocation;
  }

  /**
   * Nothing needs to happen here since we are mocking the user location along a route.
   */
  @Override
  public void requestLocationUpdates() {

  }

  /**
   * Removes location updates for the LocationListener.
   */
  @Override
  public void removeLocationUpdates() {
    for (LocationEngineListener listener : locationListeners) {
      locationListeners.remove(listener);
    }
    if (dispatcher != null) {
      dispatcher.removeReplayLocationListener(replayLocationListener);
    }
  }

  @Override
  public Type obtainType() {
    return Type.MOCK;
  }

  private void start(DirectionsRoute route) {
    handler.removeCallbacks(this);
    converter = new ReplayRouteLocationConverter(route, speed, delay);
    converter.initializeTime();
    mockedLocations = converter.toLocations();
    dispatcher = obtainDispatcher();
    dispatcher.run();
    scheduleNextDispatch();
  }

  private ReplayLocationDispatcher obtainDispatcher() {
    if (dispatcher != null) {
      dispatcher.stop();
      dispatcher.removeReplayLocationListener(replayLocationListener);
    }
    dispatcher = new ReplayLocationDispatcher(mockedLocations);
    dispatcher.addReplayLocationListener(replayLocationListener);

    return dispatcher;
  }

  private void startRoute(Point point, Location lastLocation) {
    handler.removeCallbacks(this);
    converter.updateSpeed(speed);
    converter.updateDelay(delay);
    converter.initializeTime();
    LineString route = obtainRoute(point, lastLocation);
    mockedLocations = converter.calculateMockLocations(converter.sliceRoute(route));
    dispatcher = obtainDispatcher();
    dispatcher.run();
  }

  @NonNull
  private LineString obtainRoute(Point point, Location lastLocation) {
    List<Point> pointList = new ArrayList<>();
    pointList.add(Point.fromLngLat(lastLocation.getLongitude(), lastLocation.getLatitude()));
    pointList.add(point);
    return LineString.fromLngLats(pointList);
  }

  private void scheduleNextDispatch() {
    int currentMockedPoints = mockedLocations.size();
    if (currentMockedPoints == ZERO) {
      handler.postDelayed(this, DO_NOT_DELAY);
    } else if (currentMockedPoints <= MOCKED_POINTS_LEFT_THRESHOLD) {
      handler.postDelayed(this, ONE_SECOND_IN_MILLISECONDS);
    } else {
      handler.postDelayed(this, (currentMockedPoints - MOCKED_POINTS_LEFT_THRESHOLD) * ONE_SECOND_IN_MILLISECONDS);
    }
  }

  private void initializeLastLocation() {
    if (lastLocation == null) {
      lastLocation = new Location(REPLAY_ROUTE);
    }
  }
}