// Copyright 2015 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 "content/browser/bluetooth/bluetooth_metrics.h"

#include <stdint.h>

#include <map>
#include <set>
#include <unordered_set>

#include "base/hash.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/sparse_histogram.h"
#include "device/bluetooth/bluetooth_uuid.h"

using device::BluetoothUUID;

namespace {

// Generates a hash from a canonical UUID string suitable for
// UMA_HISTOGRAM_SPARSE_SLOWLY (positive int).
//
// Hash values can be produced manually using tool: bluetooth_metrics_hash.
int HashUUID(const std::string& canonical_uuid) {
  DCHECK(canonical_uuid.size() == 36) << "HashUUID requires 128 bit UUID "
                                         "strings in canonical format to "
                                         "ensure consistent hash results.";

  // TODO(520284): Other than verifying that |uuid| contains a value, this logic
  // should be migrated to a dedicated histogram macro for hashed strings.
  uint32_t data =
      base::SuperFastHash(canonical_uuid.data(), canonical_uuid.size());

  // Strip off the sign bit because UMA doesn't support negative values,
  // but takes a signed int as input.
  return static_cast<int>(data & 0x7fffffff);
}

int HashUUID(const base::Optional<BluetoothUUID>& uuid) {
  return uuid ? HashUUID(uuid->canonical_value()) : 0;
}

}  // namespace

namespace content {

// General

void RecordWebBluetoothFunctionCall(UMAWebBluetoothFunction function) {
  UMA_HISTOGRAM_ENUMERATION("Bluetooth.Web.FunctionCall.Count",
                            static_cast<int>(function),
                            static_cast<int>(UMAWebBluetoothFunction::COUNT));
}

// requestDevice()

void RecordRequestDeviceOutcome(UMARequestDeviceOutcome outcome) {
  UMA_HISTOGRAM_ENUMERATION("Bluetooth.Web.RequestDevice.Outcome",
                            static_cast<int>(outcome),
                            static_cast<int>(UMARequestDeviceOutcome::COUNT));
}

static void RecordRequestDeviceFilters(
    const std::vector<blink::mojom::WebBluetoothScanFilterPtr>& filters) {
  UMA_HISTOGRAM_COUNTS_100("Bluetooth.Web.RequestDevice.Filters.Count",
                           filters.size());
  for (const auto& filter : filters) {
    if (!filter->services) {
      continue;
    }
    UMA_HISTOGRAM_COUNTS_100("Bluetooth.Web.RequestDevice.FilterSize",
                             filter->services->size());
    for (const BluetoothUUID& service : filter->services.value()) {
      // TODO(ortuno): Use a macro to histogram strings.
      // http://crbug.com/520284
      UMA_HISTOGRAM_SPARSE_SLOWLY(
          "Bluetooth.Web.RequestDevice.Filters.Services", HashUUID(service));
    }
  }
}

static void RecordRequestDeviceOptionalServices(
    const std::vector<BluetoothUUID>& optional_services) {
  UMA_HISTOGRAM_COUNTS_100("Bluetooth.Web.RequestDevice.OptionalServices.Count",
                           optional_services.size());
  for (const BluetoothUUID& service : optional_services) {
    // TODO(ortuno): Use a macro to histogram strings.
    // http://crbug.com/520284
    UMA_HISTOGRAM_SPARSE_SLOWLY(
        "Bluetooth.Web.RequestDevice.OptionalServices.Services",
        HashUUID(service));
  }
}

static void RecordUnionOfServices(
    const blink::mojom::WebBluetoothRequestDeviceOptionsPtr& options) {
  std::unordered_set<std::string> union_of_services;
  for (const BluetoothUUID& service : options->optional_services) {
    union_of_services.insert(service.canonical_value());
  }

  if (options->filters) {
    for (const auto& filter : options->filters.value()) {
      if (!filter->services) {
        continue;
      }
      for (const BluetoothUUID& service : filter->services.value()) {
        union_of_services.insert(service.canonical_value());
      }
    }
  }

  UMA_HISTOGRAM_COUNTS_100("Bluetooth.Web.RequestDevice.UnionOfServices.Count",
                           union_of_services.size());

  for (const std::string& service : union_of_services) {
    // TODO(ortuno): Use a macro to histogram strings.
    // http://crbug.com/520284
    UMA_HISTOGRAM_SPARSE_SLOWLY(
        "Bluetooth.Web.RequestDevice.UnionOfServices.Services",
        HashUUID(service));
  }
}

void RecordRequestDeviceOptions(
    const blink::mojom::WebBluetoothRequestDeviceOptionsPtr& options) {
  UMA_HISTOGRAM_BOOLEAN("Bluetooth.Web.RequestDevice.Options.AcceptAllDevices",
                        options->accept_all_devices);

  if (options->filters) {
    RecordRequestDeviceFilters(options->filters.value());
  }

  RecordRequestDeviceOptionalServices(options->optional_services);
  RecordUnionOfServices(options);
}

// GATTServer.Connect

void RecordConnectGATTOutcome(UMAConnectGATTOutcome outcome) {
  UMA_HISTOGRAM_ENUMERATION("Bluetooth.Web.ConnectGATT.Outcome",
                            static_cast<int>(outcome),
                            static_cast<int>(UMAConnectGATTOutcome::COUNT));
}

void RecordConnectGATTOutcome(CacheQueryOutcome outcome) {
  DCHECK(outcome == CacheQueryOutcome::NO_DEVICE);
  RecordConnectGATTOutcome(UMAConnectGATTOutcome::NO_DEVICE);
}

void RecordConnectGATTTimeSuccess(const base::TimeDelta& duration) {
  UMA_HISTOGRAM_MEDIUM_TIMES("Bluetooth.Web.ConnectGATT.TimeSuccess", duration);
}

void RecordConnectGATTTimeFailed(const base::TimeDelta& duration) {
  UMA_HISTOGRAM_MEDIUM_TIMES("Bluetooth.Web.ConnectGATT.TimeFailed", duration);
}

// getPrimaryService & getPrimaryServices

void RecordGetPrimaryServicesOutcome(
    blink::mojom::WebBluetoothGATTQueryQuantity quantity,
    UMAGetPrimaryServiceOutcome outcome) {
  switch (quantity) {
    case blink::mojom::WebBluetoothGATTQueryQuantity::SINGLE:
      UMA_HISTOGRAM_ENUMERATION(
          "Bluetooth.Web.GetPrimaryService.Outcome", static_cast<int>(outcome),
          static_cast<int>(UMAGetPrimaryServiceOutcome::COUNT));
      return;
    case blink::mojom::WebBluetoothGATTQueryQuantity::MULTIPLE:
      UMA_HISTOGRAM_ENUMERATION(
          "Bluetooth.Web.GetPrimaryServices.Outcome", static_cast<int>(outcome),
          static_cast<int>(UMAGetPrimaryServiceOutcome::COUNT));
      return;
  }
}

void RecordGetPrimaryServicesOutcome(
    blink::mojom::WebBluetoothGATTQueryQuantity quantity,
    CacheQueryOutcome outcome) {
  DCHECK(outcome == CacheQueryOutcome::NO_DEVICE);
  RecordGetPrimaryServicesOutcome(quantity,
                                  UMAGetPrimaryServiceOutcome::NO_DEVICE);
}

void RecordGetPrimaryServicesServices(
    blink::mojom::WebBluetoothGATTQueryQuantity quantity,
    const base::Optional<BluetoothUUID>& service) {
  // TODO(ortuno): Use a macro to histogram strings.
  // http://crbug.com/520284
  switch (quantity) {
    case blink::mojom::WebBluetoothGATTQueryQuantity::SINGLE:
      UMA_HISTOGRAM_SPARSE_SLOWLY("Bluetooth.Web.GetPrimaryService.Services",
                                  HashUUID(service));
      return;
    case blink::mojom::WebBluetoothGATTQueryQuantity::MULTIPLE:
      UMA_HISTOGRAM_SPARSE_SLOWLY("Bluetooth.Web.GetPrimaryServices.Services",
                                  HashUUID(service));
      return;
  }
}

// getCharacteristic & getCharacteristics

void RecordGetCharacteristicsOutcome(
    blink::mojom::WebBluetoothGATTQueryQuantity quantity,
    UMAGetCharacteristicOutcome outcome) {
  switch (quantity) {
    case blink::mojom::WebBluetoothGATTQueryQuantity::SINGLE:
      UMA_HISTOGRAM_ENUMERATION(
          "Bluetooth.Web.GetCharacteristic.Outcome", static_cast<int>(outcome),
          static_cast<int>(UMAGetCharacteristicOutcome::COUNT));
      return;
    case blink::mojom::WebBluetoothGATTQueryQuantity::MULTIPLE:
      UMA_HISTOGRAM_ENUMERATION(
          "Bluetooth.Web.GetCharacteristics.Outcome", static_cast<int>(outcome),
          static_cast<int>(UMAGetCharacteristicOutcome::COUNT));
      return;
  }
}

void RecordGetCharacteristicsOutcome(
    blink::mojom::WebBluetoothGATTQueryQuantity quantity,
    CacheQueryOutcome outcome) {
  switch (outcome) {
    case CacheQueryOutcome::SUCCESS:
    case CacheQueryOutcome::BAD_RENDERER:
      // No need to record a success or renderer crash.
      NOTREACHED();
      return;
    case CacheQueryOutcome::NO_DEVICE:
      RecordGetCharacteristicsOutcome(quantity,
                                      UMAGetCharacteristicOutcome::NO_DEVICE);
      return;
    case CacheQueryOutcome::NO_SERVICE:
      RecordGetCharacteristicsOutcome(quantity,
                                      UMAGetCharacteristicOutcome::NO_SERVICE);
      return;
    case CacheQueryOutcome::NO_CHARACTERISTIC:
      NOTREACHED();
      return;
  }
}

void RecordGetCharacteristicsCharacteristic(
    blink::mojom::WebBluetoothGATTQueryQuantity quantity,
    const base::Optional<BluetoothUUID>& characteristic) {
  switch (quantity) {
    case blink::mojom::WebBluetoothGATTQueryQuantity::SINGLE:
      UMA_HISTOGRAM_SPARSE_SLOWLY(
          "Bluetooth.Web.GetCharacteristic.Characteristic",
          HashUUID(characteristic));
      return;
    case blink::mojom::WebBluetoothGATTQueryQuantity::MULTIPLE:
      UMA_HISTOGRAM_SPARSE_SLOWLY(
          "Bluetooth.Web.GetCharacteristics.Characteristic",
          HashUUID(characteristic));
      return;
  }
}

// GATT Operations

void RecordGATTOperationOutcome(UMAGATTOperation operation,
                                UMAGATTOperationOutcome outcome) {
  switch (operation) {
    case UMAGATTOperation::CHARACTERISTIC_READ:
      RecordCharacteristicReadValueOutcome(outcome);
      return;
    case UMAGATTOperation::CHARACTERISTIC_WRITE:
      RecordCharacteristicWriteValueOutcome(outcome);
      return;
    case UMAGATTOperation::START_NOTIFICATIONS:
      RecordStartNotificationsOutcome(outcome);
      return;
    case UMAGATTOperation::COUNT:
      NOTREACHED();
      return;
  }
  NOTREACHED();
}

static UMAGATTOperationOutcome TranslateCacheQueryOutcomeToGATTOperationOutcome(
    CacheQueryOutcome outcome) {
  switch (outcome) {
    case CacheQueryOutcome::SUCCESS:
    case CacheQueryOutcome::BAD_RENDERER:
      // No need to record a success or renderer crash.
      NOTREACHED();
      return UMAGATTOperationOutcome::NOT_SUPPORTED;
    case CacheQueryOutcome::NO_DEVICE:
      return UMAGATTOperationOutcome::NO_DEVICE;
    case CacheQueryOutcome::NO_SERVICE:
      return UMAGATTOperationOutcome::NO_SERVICE;
    case CacheQueryOutcome::NO_CHARACTERISTIC:
      return UMAGATTOperationOutcome::NO_CHARACTERISTIC;
  }
  NOTREACHED() << "No need to record success or renderer crash";
  return UMAGATTOperationOutcome::NOT_SUPPORTED;
}

// Characteristic.readValue

// static
void RecordCharacteristicReadValueOutcome(UMAGATTOperationOutcome outcome) {
  UMA_HISTOGRAM_ENUMERATION("Bluetooth.Web.Characteristic.ReadValue.Outcome",
                            static_cast<int>(outcome),
                            static_cast<int>(UMAGATTOperationOutcome::COUNT));
}

void RecordCharacteristicReadValueOutcome(CacheQueryOutcome outcome) {
  RecordCharacteristicReadValueOutcome(
      TranslateCacheQueryOutcomeToGATTOperationOutcome(outcome));
}

// Characteristic.writeValue

void RecordCharacteristicWriteValueOutcome(UMAGATTOperationOutcome outcome) {
  UMA_HISTOGRAM_ENUMERATION("Bluetooth.Web.Characteristic.WriteValue.Outcome",
                            static_cast<int>(outcome),
                            static_cast<int>(UMAGATTOperationOutcome::COUNT));
}

void RecordCharacteristicWriteValueOutcome(CacheQueryOutcome outcome) {
  RecordCharacteristicWriteValueOutcome(
      TranslateCacheQueryOutcomeToGATTOperationOutcome(outcome));
}

// Characteristic.startNotifications
void RecordStartNotificationsOutcome(UMAGATTOperationOutcome outcome) {
  UMA_HISTOGRAM_ENUMERATION(
      "Bluetooth.Web.Characteristic.StartNotifications.Outcome",
      static_cast<int>(outcome),
      static_cast<int>(UMAGATTOperationOutcome::COUNT));
}

void RecordStartNotificationsOutcome(CacheQueryOutcome outcome) {
  RecordStartNotificationsOutcome(
      TranslateCacheQueryOutcomeToGATTOperationOutcome(outcome));
}

void RecordRSSISignalStrength(int rssi) {
  UMA_HISTOGRAM_SPARSE_SLOWLY("Bluetooth.Web.RequestDevice.RSSISignalStrength",
                              rssi);
}

void RecordRSSISignalStrengthLevel(UMARSSISignalStrengthLevel level) {
  UMA_HISTOGRAM_ENUMERATION(
      "Bluetooth.Web.RequestDevice.RSSISignalStrengthLevel",
      static_cast<int>(level),
      static_cast<int>(UMARSSISignalStrengthLevel::COUNT));
}

}  // namespace content
