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

#include "device/geolocation/wifi_data_provider_mac.h"

#import <CoreWLAN/CoreWLAN.h>
#import <Foundation/Foundation.h>

#include "base/mac/scoped_nsautorelease_pool.h"
#include "base/mac/scoped_nsobject.h"
#include "base/macros.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/sys_string_conversions.h"
#include "device/geolocation/wifi_data_provider_common.h"
#include "device/geolocation/wifi_data_provider_manager.h"

extern "C" NSString* const kCWScanKeyMerge;

@interface CWInterface (Private)
- (NSArray*)scanForNetworksWithParameters:(NSDictionary*)params
                                    error:(NSError**)error;
@end

namespace device {

namespace {

class CoreWlanApi : public WifiDataProviderCommon::WlanApiInterface {
 public:
  CoreWlanApi() {}

  // WlanApiInterface:
  bool GetAccessPointData(WifiData::AccessPointDataSet* data) override;

 private:
  DISALLOW_COPY_AND_ASSIGN(CoreWlanApi);
};

bool CoreWlanApi::GetAccessPointData(WifiData::AccessPointDataSet* data) {
  base::mac::ScopedNSAutoreleasePool auto_pool;
  // Initialize the scan parameters with scan key merging disabled, so we get
  // every AP listed in the scan without any SSID de-duping logic.
  NSDictionary* params = @{ kCWScanKeyMerge : @NO };

  NSSet* supported_interfaces = [CWInterface interfaceNames];
  NSUInteger interface_error_count = 0;
  for (NSString* interface_name in supported_interfaces) {
    CWInterface* corewlan_interface =
        [CWInterface interfaceWithName:interface_name];
    if (!corewlan_interface) {
      DLOG(WARNING) << interface_name << ": initWithName failed";
      ++interface_error_count;
      continue;
    }

    const base::TimeTicks start_time = base::TimeTicks::Now();

    NSError* err = nil;
    NSArray* scan =
        [corewlan_interface scanForNetworksWithParameters:params error:&err];
    const int error_code = [err code];
    const int count = [scan count];
    // We could get an error code but count != 0 if the scan was interrupted,
    // for example. For our purposes this is not fatal, so process as normal.
    if (error_code && count == 0) {
      DLOG(WARNING) << interface_name << ": CoreWLAN scan failed with error "
                    << error_code;
      ++interface_error_count;
      continue;
    }

    const base::TimeDelta duration = base::TimeTicks::Now() - start_time;

    UMA_HISTOGRAM_CUSTOM_TIMES("Net.Wifi.ScanLatency", duration,
                               base::TimeDelta::FromMilliseconds(1),
                               base::TimeDelta::FromMinutes(1), 100);

    DVLOG(1) << interface_name << ": found " << count << " wifi APs";

    for (CWNetwork* network in scan) {
      DCHECK(network);
      AccessPointData access_point_data;
      // -[CWNetwork bssid] uses colons to separate the components of the MAC
      // address, but AccessPointData requires they be separated with a dash.
      access_point_data.mac_address = base::SysNSStringToUTF16([[network bssid]
          stringByReplacingOccurrencesOfString:@":"
                                    withString:@"-"]);
      access_point_data.radio_signal_strength = [network rssiValue];
      access_point_data.channel = [[network wlanChannel] channelNumber];
      access_point_data.signal_to_noise =
          access_point_data.radio_signal_strength - [network noiseMeasurement];
      access_point_data.ssid = base::SysNSStringToUTF16([network ssid]);
      data->insert(access_point_data);
    }
  }

  UMA_HISTOGRAM_CUSTOM_COUNTS(
      "Net.Wifi.InterfaceCount",
      [supported_interfaces count] - interface_error_count, 1, 5, 6);

  // Return true even if some interfaces failed to scan, so long as at least
  // one interface did not fail.
  return interface_error_count == 0 ||
         [supported_interfaces count] > interface_error_count;
};

// The time periods, in milliseconds, between successive polls of the wifi data.
const int kDefaultPollingInterval = 120000;                // 2 mins
const int kNoChangePollingInterval = 300000;               // 5 mins
const int kTwoNoChangePollingInterval = 600000;            // 10 mins
const int kNoWifiPollingIntervalMilliseconds = 20 * 1000;  // 20s

}  // namespace

// static
WifiDataProvider* WifiDataProviderManager::DefaultFactoryFunction() {
  return new WifiDataProviderMac();
}

WifiDataProviderMac::WifiDataProviderMac() {}

WifiDataProviderMac::~WifiDataProviderMac() {}

WifiDataProviderMac::WlanApiInterface* WifiDataProviderMac::NewWlanApi() {
  return new CoreWlanApi();
}

WifiPollingPolicy* WifiDataProviderMac::NewPollingPolicy() {
  return new GenericWifiPollingPolicy<
      kDefaultPollingInterval, kNoChangePollingInterval,
      kTwoNoChangePollingInterval, kNoWifiPollingIntervalMilliseconds>;
}

}  // namespace device
