// Copyright 2021 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/system_geolocation_source_apple.h"

#include <memory>

#include "base/functional/callback_helpers.h"
#include "base/mac/mac_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/task/single_thread_task_runner.h"
#include "build/build_config.h"
#include "components/device_event_log/device_event_log.h"
#include "services/device/public/cpp/geolocation/location_manager_delegate.h"

#if BUILDFLAG(IS_MAC)
#import <CoreWLAN/CoreWLAN.h>
#endif

namespace device {

std::optional<bool> SystemGeolocationSourceApple::mock_wifi_status_;

#if BUILDFLAG(IS_IOS_TVOS)
// This delay is used to throttle location requests.
constexpr base::TimeDelta kRequestLocationThrottleDelay =
    base::Milliseconds(1000);
#endif

// static
bool SystemGeolocationSourceApple::IsWifiEnabled() {
  if (mock_wifi_status_.has_value()) {
    return *mock_wifi_status_;
  }
#if BUILDFLAG(IS_MAC)
  CWWiFiClient* wifi_client = [CWWiFiClient sharedWiFiClient];
  CWInterface* interface = wifi_client.interface;
  return (interface && interface.powerOn);
#else
  return true;
#endif
}

SystemGeolocationSourceApple::SystemGeolocationSourceApple()
    : location_manager_([[CLLocationManager alloc] init]),
      permission_update_callback_(base::DoNothing()),
      position_observers_(base::MakeRefCounted<PositionObserverList>()),
      main_task_runner_(base::SingleThreadTaskRunner::GetCurrentDefault()) {
  delegate_ = [[LocationManagerDelegate alloc]
      initWithManager:weak_ptr_factory_.GetWeakPtr()];
  location_manager_.delegate = delegate_;
}

SystemGeolocationSourceApple::~SystemGeolocationSourceApple() = default;

// static
std::unique_ptr<GeolocationSystemPermissionManager>
SystemGeolocationSourceApple::CreateGeolocationSystemPermissionManager() {
  return std::make_unique<GeolocationSystemPermissionManager>(
      std::make_unique<SystemGeolocationSourceApple>());
}

void SystemGeolocationSourceApple::RegisterPermissionUpdateCallback(
    PermissionUpdateCallback callback) {
  permission_update_callback_ = callback;
  permission_update_callback_.Run(GetSystemPermission());
}

void SystemGeolocationSourceApple::PermissionUpdated() {
  permission_update_callback_.Run(GetSystemPermission());
}

void SystemGeolocationSourceApple::PositionUpdated(
    const mojom::Geoposition& position) {
  CHECK(main_task_runner_->BelongsToCurrentThread());

  // Record the time to first position update. This is only done once to capture
  // the initial position acquisition time.
  if (!position_received_) {
    const base::TimeDelta time_to_first_position =
        base::TimeTicks::Now() - watch_start_time_;
    UmaHistogramCustomTimes(
        "Geolocation.CoreLocationProvider.TimeToFirstPosition",
        time_to_first_position, base::Milliseconds(1), base::Seconds(10), 100);
    position_received_ = true;
  }

  position_observers_->Notify(FROM_HERE, &PositionObserver::OnPositionUpdated,
                              position);
}

void SystemGeolocationSourceApple::PositionError(
    const mojom::GeopositionError& error) {
  CHECK(main_task_runner_->BelongsToCurrentThread());
  if (!session_result_) {
    session_result_ = CoreLocationSessionResult::kCoreLocationError;
  }
  // If an error reported from `LocationManagerDelegate` when
  // network change timer is running. Stop the timer (which also cancel the
  // pending fallback) and report that error.
  if (network_changed_timer_.IsRunning()) {
    GEOLOCATION_LOG(DEBUG) << "SystemGeolocationSourceApple::PositionError: "
                              "Network status change timer is cancelled.";
    network_changed_timer_.Stop();
    net::NetworkChangeNotifier::RemoveNetworkChangeObserver(this);
  }
  position_observers_->Notify(FROM_HERE, &PositionObserver::OnPositionError,
                              error);
}

#if BUILDFLAG(IS_IOS_TVOS)
void SystemGeolocationSourceApple::OnRequestLocationThrottleTimerFiring() {
  [location_manager_ requestLocation];
}
#endif

void SystemGeolocationSourceApple::StartWatchingPositionInternal(
    bool high_accuracy) {
  CHECK(main_task_runner_->BelongsToCurrentThread());
  watch_start_time_ = base::TimeTicks::Now();
  if (high_accuracy) {
    location_manager_.desiredAccuracy = kCLLocationAccuracyBest;
  } else {
    // Using kCLLocationAccuracyHundredMeters for consistency with Android.
    location_manager_.desiredAccuracy = kCLLocationAccuracyHundredMeters;
  }
#if BUILDFLAG(IS_IOS_TVOS)
  // tvOS uses requestLocation for the one-time delivery of the location.
  // Since requestLocation updates location every call,
  // `request_throttle_timer_` is used to throttle requests so that it
  // mitigates location update by a client.
  request_throttle_timer_.Start(
      FROM_HERE, kRequestLocationThrottleDelay,
      base::BindOnce(
          &SystemGeolocationSourceApple::OnRequestLocationThrottleTimerFiring,
          base::Unretained(this)));
#else
  [location_manager_ startUpdatingLocation];
#endif
  was_wifi_enabled_ = IsWifiEnabled();
}

void SystemGeolocationSourceApple::StartWatchingPosition(bool high_accuracy) {
  main_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(
          &SystemGeolocationSourceApple::StartWatchingPositionInternal,
          weak_ptr_factory_.GetWeakPtr(), high_accuracy));
}

void SystemGeolocationSourceApple::StopWatchingPositionInternal() {
#if BUILDFLAG(IS_IOS_TVOS)
  request_throttle_timer_.Stop();
#endif
  CHECK(main_task_runner_->BelongsToCurrentThread());
  [location_manager_ stopUpdatingLocation];
  // If `StopWatchingPosition` is called for any reason, stop the network status
  // event timer (which also cancel the pending fallback).
  if (network_changed_timer_.IsRunning()) {
    GEOLOCATION_LOG(DEBUG)
        << "SystemGeolocationSourceApple::StopWatchingPositionInternal: "
           "Network status change timer is cancelled.";
    network_changed_timer_.Stop();
    net::NetworkChangeNotifier::RemoveNetworkChangeObserver(this);
  }

  // Record the session result if either:
  // 1. An error occurred (session_result_ is set).
  // 2. At least one position update was received (position_received_ is true).
  // This excludes short-lived sessions that start and stop immediately
  // without obtaining any position updates.
  if (session_result_ || position_received_) {
    base::UmaHistogramSparse("Geolocation.CoreLocationProvider.SessionResult",
                             static_cast<int>(session_result_.value_or(
                                 CoreLocationSessionResult::kSuccess)));
  }
  session_result_.reset();
  position_received_ = false;
}

void SystemGeolocationSourceApple::StopWatchingPosition() {
  main_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(
          &SystemGeolocationSourceApple::StopWatchingPositionInternal,
          weak_ptr_factory_.GetWeakPtr()));
}

LocationSystemPermissionStatus
SystemGeolocationSourceApple::GetSystemPermission() const {
  CHECK(main_task_runner_->BelongsToCurrentThread());
  if (![delegate_ permissionInitialized]) {
    return LocationSystemPermissionStatus::kNotDetermined;
  }

  if ([delegate_ hasPermission]) {
    return LocationSystemPermissionStatus::kAllowed;
  }

  return LocationSystemPermissionStatus::kDenied;
}

void SystemGeolocationSourceApple::OpenSystemPermissionSetting() {
#if BUILDFLAG(IS_MAC)
  base::mac::OpenSystemSettingsPane(
      base::mac::SystemSettingsPane::kPrivacySecurity_LocationServices);
#endif
}

void SystemGeolocationSourceApple::RequestPermission() {
  [location_manager_ requestWhenInUseAuthorization];
}

void SystemGeolocationSourceApple::AddPositionUpdateObserver(
    PositionObserver* observer) {
  position_observers_->AddObserver(observer);
}

void SystemGeolocationSourceApple::RemovePositionUpdateObserver(
    PositionObserver* observer) {
  position_observers_->RemoveObserver(observer);
}

void SystemGeolocationSourceApple::OnNetworkChanged(
    net::NetworkChangeNotifier::ConnectionType type) {
  CHECK(main_task_runner_->BelongsToCurrentThread());
  // When Wi-Fi is disabled, we initially receive a
  // `net::NetworkChangeNotifier::CONNECTION_NONE` event. Subsequently, after
  // the network stabilizes, another network change event will be triggered
  // (potentially any connection type except
  // `net::NetworkChangeNotifier::CONNECTION_NONE`).
  GEOLOCATION_LOG(DEBUG) << "SystemGeolocationSourceApple::OnNetworkChanged: "
                            "Invoked with connection type = "
                         << static_cast<int>(type);
  if (type != net::NetworkChangeNotifier::CONNECTION_NONE &&
      network_changed_timer_.IsRunning()) {
    GEOLOCATION_LOG(DEBUG)
        << "SystemGeolocationSourceApple::OnNetworkChanged: Network status is "
           "settled, create error position with kWifiDisabled error code to "
           "start fallback mechanism.";
    network_changed_timer_.Stop();
    net::NetworkChangeNotifier::RemoveNetworkChangeObserver(this);

    device::mojom::GeopositionError position_error;
    position_error.error_code =
        device::mojom::GeopositionErrorCode::kWifiDisabled;
    position_error.error_message =
        device::mojom::kGeoPositionUnavailableErrorMessage;
    position_error.error_technical =
        "CoreLocationProvider: CoreLocation framework reported a "
        "kWifiDisabled error.";
    session_result_ = CoreLocationSessionResult::kWifiDisabled;
    PositionError(position_error);
  }
}

void SystemGeolocationSourceApple::StartNetworkChangedTimer() {
  CHECK(main_task_runner_->BelongsToCurrentThread());
  if (network_changed_timer_.IsRunning()) {
    GEOLOCATION_LOG(DEBUG)
        << "SystemGeolocationSourceApple::StartNetworkChangedTimer: Network "
           "status change timer is already running, ignore this call.";
    return;
  }
  GEOLOCATION_LOG(DEBUG)
      << "SystemGeolocationSourceApple::StartNetworkChangedTimer: Network "
         "status change timer is started.";

  net::NetworkChangeNotifier::AddNetworkChangeObserver(this);

  // This timer prevents premature fallback initiation if the network state is
  // unstable. Empirically, network change events occur ~600-700ms after Wi-Fi
  // is disabled. A 1-second timeout accommodates potential variations across
  // different machines.
  network_changed_timer_.Start(
      FROM_HERE, base::Milliseconds(kNetworkChangeTimeoutMs), this,
      &SystemGeolocationSourceApple::OnNetworkChangedTimeout);
}

void SystemGeolocationSourceApple::OnNetworkChangedTimeout() {
  CHECK(main_task_runner_->BelongsToCurrentThread());
  net::NetworkChangeNotifier::RemoveNetworkChangeObserver(this);
  GEOLOCATION_LOG(DEBUG)
      << "SystemGeolocationSourceApple::OnNetworkChangedTimeout: Network "
         "status change timer timed out.";

  device::mojom::GeopositionError position_error;
  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.";
  PositionError(position_error);
}

}  // namespace device
