// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "services/device/public/cpp/geolocation/location_manager_delegate.h"

#include "base/feature_list.h"
#include "base/metrics/histogram_functions.h"
#include "components/device_event_log/device_event_log.h"
#include "services/device/public/cpp/device_features.h"
#include "services/device/public/cpp/geolocation/system_geolocation_source_apple.h"

@implementation LocationManagerDelegate

- (instancetype)initWithManager:
    (base::WeakPtr<device::SystemGeolocationSourceApple>)manager {
  if ((self = [super init])) {
    _permissionInitialized = NO;
    _hasPermission = NO;
    _manager = manager;
  }
  return self;
}

- (void)locationManager:(CLLocationManager*)manager
    didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
  if (status == kCLAuthorizationStatusNotDetermined) {
    _permissionInitialized = NO;
    return;
  }
  _permissionInitialized = YES;
  if (status == kCLAuthorizationStatusAuthorizedAlways) {
    _hasPermission = YES;
  } else {
    _hasPermission = NO;
  }

#if BUILDFLAG(IS_IOS)
  if (status == kCLAuthorizationStatusAuthorizedWhenInUse) {
    _hasPermission = YES;
  }
#endif

  _manager->PermissionUpdated();
}

- (BOOL)hasPermission {
  return _hasPermission;
}

- (BOOL)permissionInitialized {
  return _permissionInitialized;
}

- (void)locationManager:(CLLocationManager*)manager
     didUpdateLocations:(NSArray*)locations {
  CLLocation* location = [locations lastObject];
  device::mojom::Geoposition position;
  position.latitude = location.coordinate.latitude;
  position.longitude = location.coordinate.longitude;
  position.timestamp = base::Time::FromSecondsSinceUnixEpoch(
      location.timestamp.timeIntervalSince1970);
  if (base::FeatureList::IsEnabled(features::kEllipsoidalAltitude)) {
    position.altitude = location.ellipsoidalAltitude;
  } else {
    position.altitude = location.altitude;
  }
  position.accuracy = location.horizontalAccuracy;
  position.altitude_accuracy = location.verticalAccuracy;
  position.speed = location.speed;
  position.heading = location.course;

  // Records the accuracy value (in meters) of a valid Geoposition.
  // Values above 10000 meters are considered very inaccurate and are
  // categorized into the overflow bucket. This cap prioritizes accuracy
  // resolution in the lower range.
  base::UmaHistogramCounts10000("Geolocation.CoreLocationProvider.Accuracy",
                                static_cast<int>(position.accuracy));
  _manager->PositionUpdated(position);
}

- (void)locationManager:(CLLocationManager*)manager
       didFailWithError:(NSError*)error {
  base::UmaHistogramSparse("Geolocation.CoreLocationProvider.ErrorCode",
                           static_cast<int>(error.code));
  GEOLOCATION_LOG(ERROR)
      << "CLLocationManager::didFailWithError invoked with error code: "
      << static_cast<int>(error.code);

  device::mojom::GeopositionError position_error;
  switch (error.code) {
    case kCLErrorDenied:
      position_error.error_code =
          device::mojom::GeopositionErrorCode::kPermissionDenied;
      position_error.error_message =
          device::mojom::kGeoPermissionDeniedErrorMessage;
      position_error.error_technical =
          "CoreLocationProvider: CoreLocation framework reported a "
          "kCLErrorDenied failure.";
      break;
    case kCLErrorPromptDeclined:
      position_error.error_code =
          device::mojom::GeopositionErrorCode::kPermissionDenied;
      position_error.error_message =
          device::mojom::kGeoPermissionDeniedErrorMessage;
      position_error.error_technical =
          "CoreLocationProvider: CoreLocation framework reported a "
          "kCLErrorPromptDeclined failure.";
      break;
    case kCLErrorLocationUnknown: {
      if (!_manager->WasWifiEnabled()) {
        // If Wi-Fi was already disabled when `StartWatchingPosition` was
        // called, we can immediately trigger the fallback mechanism by
        // reporting a `kWifiDisabled` error.
        position_error.error_code =
            device::mojom::GeopositionErrorCode::kWifiDisabled;
      } else if (!_manager->IsWifiEnabled()) {
        // If Wi-Fi was enabled but is now disabled when
        // `kCLErrorLocationUnknown` is reported, initiate the fallback
        // mechanism after the network changed event fires. This ensures the
        // initial network request doesn't fail due to an unsettled network
        // state.
        _manager->StartNetworkChangedTimer();
        return;
      } else {
        // In all other cases, let the `kPositionUnavailable` error propagate.
        position_error.error_code =
            device::mojom::GeopositionErrorCode::kPositionUnavailable;
      }
      position_error.error_message =
          device::mojom::kGeoPositionUnavailableErrorMessage;
      position_error.error_technical =
          "CoreLocationProvider: CoreLocation framework reported a "
          "kCLErrorLocationUnknown failure.";
      break;
    }
    case kCLErrorNetwork:
      position_error.error_code =
          device::mojom::GeopositionErrorCode::kPositionUnavailable;
      position_error.error_message =
          device::mojom::kGeoPositionUnavailableErrorMessage;
      position_error.error_technical =
          "CoreLocationProvider: CoreLocation framework reported a "
          "kCLErrorNetwork failure.";
      break;
    default:
      // For non-critical errors (heading, ranging, or region monitoring),
      // return immediately without setting the position_error, as they may not
      // be relevant to the caller.
      return;
  }

  _manager->PositionError(position_error);
}

@end
