#include "SensorManager.h"

#if ENV_INCLUDE_GPS
#ifndef GPS_TELEMETRY_CACHE_INTERVAL_SEC
#define GPS_TELEMETRY_CACHE_INTERVAL_SEC (2UL * 60UL * 60UL)
#endif
#ifndef GPS_TELEMETRY_MAX_ACQUIRE_SEC
#define GPS_TELEMETRY_MAX_ACQUIRE_SEC (15UL * 60UL)
#endif
#ifndef GPS_TELEMETRY_STABLE_SEC
#define GPS_TELEMETRY_STABLE_SEC 30UL
#endif
#ifndef GPS_TELEMETRY_HOLD_SEC
#define GPS_TELEMETRY_HOLD_SEC (2UL * 60UL * 60UL)
#endif
#ifndef GPS_TELEMETRY_MAX_STALE_SEC
#define GPS_TELEMETRY_MAX_STALE_SEC (12UL * 60UL * 60UL)
#endif
#ifndef GPS_TELEMETRY_STABLE_RADIUS_M
#define GPS_TELEMETRY_STABLE_RADIUS_M 100.0f
#endif

static bool millisDue(unsigned long now, unsigned long deadline) {
  return (long)(now - deadline) >= 0;
}

static float gpsDistanceSquaredMeters(float lat1, float lon1, float lat2, float lon2) {
  static const float METERS_PER_DEGREE = 111320.0f;
  float x = (lon2 - lon1) * METERS_PER_DEGREE;
  float y = (lat2 - lat1) * METERS_PER_DEGREE;
  return x * x + y * y;
}

bool SensorManager::gpsTelemetryHoldActive(unsigned long now) const {
  return gps_hold_until != 0 && !millisDue(now, gps_hold_until);
}

bool SensorManager::gpsTelemetryCacheFresh(unsigned long now) const {
  return gps_cache_valid &&
      (unsigned long)(now - gps_cache_updated_at) <= GPS_TELEMETRY_MAX_STALE_SEC * 1000UL;
}

void SensorManager::updateGpsTelemetryCache(float lat, float lon, float altitude, unsigned long now) {
  gps_cache_lat = lat;
  gps_cache_lon = lon;
  gps_cache_altitude = altitude;
  gps_cache_updated_at = now;
  gps_cache_valid = true;
}

void SensorManager::maybeStopGpsForTelemetry(unsigned long now) {
#if defined(PERSISTANT_GPS) || defined(FORCE_GPS_ALIVE)
  (void)now;
#else
  if (telemetryGpsActive() && !gps_user_enabled && !gps_acquiring && !gpsTelemetryHoldActive(now)) {
    telemetryGpsStop();
    gps_next_cache_update_at = now + GPS_TELEMETRY_CACHE_INTERVAL_SEC * 1000UL;
  }
#endif
}

void SensorManager::beginGpsTelemetryAcquisition(unsigned long now) {
  if (!telemetryGpsDetected() || gps_acquiring) return;

  gps_acquiring = true;
  gps_acquire_has_fix = false;
  gps_acquire_started_at = now;
  gps_stable_started_at = now;
  gps_weighted_lat = 0;
  gps_weighted_lon = 0;
  gps_weighted_altitude = 0;
  gps_weight_sum = 0;
  gps_weight_count = 0;
  if (!telemetryGpsActive()) telemetryGpsStart();
}

void SensorManager::finishGpsTelemetryAcquisition(unsigned long now, bool use_weighted_average) {
  if (use_weighted_average && gps_weight_sum > 0) {
    updateGpsTelemetryCache(gps_weighted_lat / gps_weight_sum,
                            gps_weighted_lon / gps_weight_sum,
                            gps_weighted_altitude / gps_weight_sum,
                            now);
  }
  gps_acquiring = false;
  gps_next_cache_update_at = now + GPS_TELEMETRY_CACHE_INTERVAL_SEC * 1000UL;
  maybeStopGpsForTelemetry(now);
}

bool SensorManager::queryGpsTelemetry(uint8_t requester_permissions, CayenneLPP& telemetry) {
  if (!(requester_permissions & TELEM_PERM_LOCATION) || !telemetryGpsDetected()) return false;

  unsigned long now = millis();
  gps_hold_until = now + GPS_TELEMETRY_HOLD_SEC * 1000UL;
  if (!telemetryGpsActive()) telemetryGpsStart();
  if (!gpsTelemetryCacheFresh(now) && !gps_acquiring) beginGpsTelemetryAcquisition(now);

  if (!gpsTelemetryCacheFresh(now)) return false;
  telemetry.addGPS(TELEM_CHANNEL_SELF, gps_cache_lat, gps_cache_lon, gps_cache_altitude);
  return true;
}

void SensorManager::processGpsTelemetryFix(float lat, float lon, float altitude, unsigned long now) {
  if (!gps_acquiring) {
    if (gps_user_enabled || gpsTelemetryHoldActive(now)) {
      updateGpsTelemetryCache(lat, lon, altitude, now);
    }
    return;
  }

  if (!gps_acquire_has_fix) {
    gps_acquire_has_fix = true;
    gps_stable_started_at = now;
    gps_stable_origin_lat = lat;
    gps_stable_origin_lon = lon;
  }

  static const float STABLE_RADIUS_M2 =
      GPS_TELEMETRY_STABLE_RADIUS_M * GPS_TELEMETRY_STABLE_RADIUS_M;
  if (gpsDistanceSquaredMeters(gps_stable_origin_lat, gps_stable_origin_lon, lat, lon) > STABLE_RADIUS_M2) {
    updateGpsTelemetryCache(lat, lon, altitude, now);
    finishGpsTelemetryAcquisition(now, false);
    return;
  }

  float weight = (float)++gps_weight_count;
  gps_weighted_lat += lat * weight;
  gps_weighted_lon += lon * weight;
  gps_weighted_altitude += altitude * weight;
  gps_weight_sum += weight;
  if (millisDue(now, gps_stable_started_at + GPS_TELEMETRY_STABLE_SEC * 1000UL)) {
    finishGpsTelemetryAcquisition(now, true);
  }
}

void SensorManager::loopGpsTelemetry(unsigned long now) {
  if (!gps_user_enabled && !gpsTelemetryHoldActive(now) && !gps_acquiring) {
    maybeStopGpsForTelemetry(now);
  }
  if (gps_location_access_available && telemetryGpsDetected() && !gps_user_enabled &&
      !gpsTelemetryHoldActive(now) && !gps_acquiring &&
      (gps_next_cache_update_at == 0 || millisDue(now, gps_next_cache_update_at))) {
    beginGpsTelemetryAcquisition(now);
  }
  if (gps_acquiring && millisDue(now, gps_acquire_started_at + GPS_TELEMETRY_MAX_ACQUIRE_SEC * 1000UL)) {
    finishGpsTelemetryAcquisition(now, gps_acquire_has_fix && gps_weight_sum > 0);
  }
}

void SensorManager::setGpsTelemetryUserEnabled(bool enabled) {
  gps_user_enabled = enabled;
  unsigned long now = millis();
  if (enabled) {
    if (telemetryGpsDetected() && !telemetryGpsActive()) telemetryGpsStart();
  } else {
    maybeStopGpsForTelemetry(now);
  }
}
#endif

void SensorManager::setTelemetryLocationAccessAvailable(bool available) {
#if ENV_INCLUDE_GPS
  unsigned long now = millis();
  if (gps_location_access_available == available) return;

  gps_location_access_available = available;
  if (available) {
    gps_next_cache_update_at = 0;
  } else if (gps_acquiring && !gps_user_enabled && !gpsTelemetryHoldActive(now)) {
    gps_acquiring = false;
    maybeStopGpsForTelemetry(now);
  }
#else
  (void)available;
#endif
}
