// 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 "base/power_monitor/battery_level_provider.h"

#include <initguid.h>
#include <windows.h>  // Must be in front of other Windows header files.

#include <devguid.h>
#include <poclass.h>
#include <setupapi.h>
#include <winioctl.h>

#include <algorithm>
#include <array>
#include <vector>

#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/numerics/safe_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/scoped_blocking_call.h"
#include "base/win/scoped_devinfo.h"
#include "base/win/scoped_handle.h"

namespace base {
namespace {

// Returns a handle to the battery interface identified by |interface_data|, or
// nullopt if the request failed. |devices| is a device information set that
// contains battery devices information, obtained with ::SetupDiGetClassDevs().
base::win::ScopedHandle GetBatteryHandle(
    HDEVINFO devices,
    SP_DEVICE_INTERFACE_DATA* interface_data) {
  // Query size required to hold |interface_detail|.
  DWORD required_size = 0;
  ::SetupDiGetDeviceInterfaceDetail(devices, interface_data, nullptr, 0,
                                    &required_size, nullptr);
  DWORD error = ::GetLastError();
  if (error != ERROR_INSUFFICIENT_BUFFER)
    return base::win::ScopedHandle();

  // |interface_detail->DevicePath| is variable size.
  std::vector<uint8_t> raw_buf(required_size);
  auto* interface_detail =
      reinterpret_cast<SP_DEVICE_INTERFACE_DETAIL_DATA*>(raw_buf.data());
  interface_detail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);

  BOOL success = ::SetupDiGetDeviceInterfaceDetail(
      devices, interface_data, interface_detail, required_size, nullptr,
      nullptr);
  if (!success)
    return base::win::ScopedHandle();

  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);
  base::win::ScopedHandle battery(
      ::CreateFile(interface_detail->DevicePath, GENERIC_READ | GENERIC_WRITE,
                   FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING,
                   FILE_ATTRIBUTE_NORMAL, nullptr));
  return battery;
}

// Returns the current tag for `battery` handle, BATTERY_TAG_INVALID if there is
// no battery present in this interface or nullopt on retrieval error.
// See
// https://docs.microsoft.com/en-us/windows/win32/power/ioctl-battery-query-tag
absl::optional<ULONG> GetBatteryTag(HANDLE battery) {
  ULONG battery_tag = 0;
  ULONG wait = 0;
  DWORD bytes_returned = 0;
  BOOL success = ::DeviceIoControl(
      battery, IOCTL_BATTERY_QUERY_TAG, &wait, sizeof(wait), &battery_tag,
      sizeof(battery_tag), &bytes_returned, nullptr);
  if (!success) {
    if (::GetLastError() == ERROR_FILE_NOT_FOUND) {
      // No battery present in this interface.
      //
      // TODO(crbug.com/1191045): Change CHECK to DCHECK in October 2022 after
      // verifying that there are no crash reports.
      CHECK_EQ(battery_tag, static_cast<ULONG>(BATTERY_TAG_INVALID));
      return battery_tag;
    }
    // Retrieval error.
    return absl::nullopt;
  }
  return battery_tag;
}

// Returns BATTERY_INFORMATION structure containing battery information, given
// battery handle and tag, or nullopt if the request failed. Battery handle and
// tag are obtained with GetBatteryHandle() and GetBatteryTag(), respectively.
absl::optional<BATTERY_INFORMATION> GetBatteryInformation(HANDLE battery,
                                                          ULONG battery_tag) {
  BATTERY_QUERY_INFORMATION query_information = {};
  query_information.BatteryTag = battery_tag;
  query_information.InformationLevel = BatteryInformation;
  BATTERY_INFORMATION battery_information = {};
  DWORD bytes_returned;
  BOOL success = ::DeviceIoControl(
      battery, IOCTL_BATTERY_QUERY_INFORMATION, &query_information,
      sizeof(query_information), &battery_information,
      sizeof(battery_information), &bytes_returned, nullptr);
  if (!success)
    return absl::nullopt;
  return battery_information;
}

// Returns the granularity of the battery discharge.
absl::optional<uint32_t> GetBatteryBatteryDischargeGranularity(
    HANDLE battery,
    ULONG battery_tag,
    ULONG current_capacity,
    ULONG designed_capacity) {
  BATTERY_QUERY_INFORMATION query_information = {};
  query_information.BatteryTag = battery_tag;
  query_information.InformationLevel = BatteryGranularityInformation;

  // The battery discharge granularity can change as the level of the battery
  // gets closer to zero. The documentation for `BatteryGranularityInformation`
  // says that a maximum of 4 scales is possible. Each scale contains the
  // granularity (in mWh) and the capacity (in mWh) at which the scale takes
  // effect.
  // https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-battery_reporting_scale
  std::array<BATTERY_REPORTING_SCALE, 4> battery_reporting_scales;

  DWORD bytes_returned = 0;
  BOOL success = ::DeviceIoControl(
      battery, IOCTL_BATTERY_QUERY_INFORMATION, &query_information,
      sizeof(query_information), &battery_reporting_scales,
      sizeof(battery_reporting_scales), &bytes_returned, nullptr);
  if (!success)
    return absl::nullopt;

  ptrdiff_t nb_elements = base::checked_cast<ptrdiff_t>(
      bytes_returned / sizeof(BATTERY_REPORTING_SCALE));
  if (!nb_elements)
    return absl::nullopt;

  // The granularities are ordered from the highest capacity to the lowest
  // capacity, or from the most coarse granularity to the most precise
  // granularity, according to the documentation.
  // Just in case, the documentation is not trusted for |max_granularity|. All
  // the values are still compared to find the most coarse granularity.
  DWORD max_granularity =
      std::max_element(std::begin(battery_reporting_scales),
                       std::begin(battery_reporting_scales) + nb_elements,
                       [](const auto& lhs, const auto& rhs) {
                         return lhs.Granularity < rhs.Granularity;
                       })
          ->Granularity;

  // Check if the API can be trusted, which would simplify the implementation of
  // this function.
  UMA_HISTOGRAM_BOOLEAN(
      "Power.BatteryDischargeGranularityIsOrdered",
      max_granularity == battery_reporting_scales[0].Granularity);

  return max_granularity;
}

// Returns BATTERY_STATUS structure containing battery state, given battery
// handle and tag, or nullopt if the request failed. Battery handle and tag are
// obtained with GetBatteryHandle() and GetBatteryTag(), respectively.
absl::optional<BATTERY_STATUS> GetBatteryStatus(HANDLE battery,
                                                ULONG battery_tag) {
  BATTERY_WAIT_STATUS wait_status = {};
  wait_status.BatteryTag = battery_tag;
  BATTERY_STATUS battery_status;
  DWORD bytes_returned;
  BOOL success = ::DeviceIoControl(
      battery, IOCTL_BATTERY_QUERY_STATUS, &wait_status, sizeof(wait_status),
      &battery_status, sizeof(battery_status), &bytes_returned, nullptr);
  if (!success)
    return absl::nullopt;
  return battery_status;
}

}  // namespace

class BatteryLevelProviderWin : public BatteryLevelProvider {
 public:
  BatteryLevelProviderWin() = default;
  ~BatteryLevelProviderWin() override = default;

  void GetBatteryState(
      base::OnceCallback<void(const absl::optional<BatteryState>&)> callback)
      override {
    // This is run on |blocking_task_runner_| since `GetBatteryStateImpl()` has
    // blocking calls and can take several seconds to complete.
    blocking_task_runner_->PostTaskAndReplyWithResult(
        FROM_HERE,
        base::BindOnce(&BatteryLevelProviderWin::GetBatteryStateImpl),
        base::BindOnce(&BatteryLevelProviderWin::OnBatteryStateObtained,
                       weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
  }

 private:
  static absl::optional<BatteryState> GetBatteryStateImpl();

  void OnBatteryStateObtained(
      base::OnceCallback<void(const absl::optional<BatteryState>&)> callback,
      const absl::optional<BatteryState>& battery_state) {
    std::move(callback).Run(battery_state);
  }

  // TaskRunner used to run blocking `GetBatteryStateImpl()` queries, sequenced
  // to avoid the performance cost of concurrent calls.
  scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_{
      base::ThreadPool::CreateSequencedTaskRunner(
          {base::MayBlock(),
           base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN})};

  base::WeakPtrFactory<BatteryLevelProviderWin> weak_ptr_factory_{this};
};

std::unique_ptr<BatteryLevelProvider> BatteryLevelProvider::Create() {
  return std::make_unique<BatteryLevelProviderWin>();
}

// static
absl::optional<BatteryLevelProvider::BatteryState>
BatteryLevelProviderWin::GetBatteryStateImpl() {
  // Proactively mark as blocking to fail early, since calls below may also
  // trigger ScopedBlockingCall.
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);

  // Battery interfaces are enumerated at every sample to detect when a new
  // interface is added, and avoid holding dangling handles when a battery is
  // disconnected.
  base::win::ScopedDevInfo devices(::SetupDiGetClassDevs(
      &GUID_DEVICE_BATTERY, 0, 0, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE));
  if (!devices.is_valid()) {
    return absl::nullopt;
  }

  std::vector<BatteryDetails> battery_details_list;

  // The algorithm to enumerate battery devices is taken from
  // https://docs.microsoft.com/en-us/windows/win32/power/enumerating-battery-devices
  // Limit search to 8 batteries max. A system may have several battery slots
  // and each slot may hold an actual battery.
  for (DWORD device_index = 0; device_index < 8; ++device_index) {
    SP_DEVICE_INTERFACE_DATA interface_data = {};
    interface_data.cbSize = sizeof(interface_data);

    BOOL success =
        ::SetupDiEnumDeviceInterfaces(devices.get(), 0, &GUID_DEVCLASS_BATTERY,
                                      device_index, &interface_data);
    if (!success) {
      // Enumeration ended normally.
      if (::GetLastError() == ERROR_NO_MORE_ITEMS)
        break;
      // Error.
      return absl::nullopt;
    }

    base::win::ScopedHandle battery =
        GetBatteryHandle(devices.get(), &interface_data);
    if (!battery.IsValid())
      return absl::nullopt;

    absl::optional<ULONG> battery_tag = GetBatteryTag(battery.Get());
    if (!battery_tag.has_value()) {
      return absl::nullopt;
    } else if (battery_tag.value() == BATTERY_TAG_INVALID) {
      // No battery present in this interface.
      continue;
    }

    auto battery_information =
        GetBatteryInformation(battery.Get(), *battery_tag);
    if (!battery_information.has_value()) {
      return absl::nullopt;
    }

    auto battery_status = GetBatteryStatus(battery.Get(), *battery_tag);
    if (!battery_status.has_value()) {
      return absl::nullopt;
    }

    absl::optional<uint32_t> battery_discharge_granularity =
        GetBatteryBatteryDischargeGranularity(
            battery.Get(), *battery_tag, battery_status->Capacity,
            battery_information->DesignedCapacity);

    battery_details_list.push_back(BatteryDetails(
        {.is_external_power_connected =
             !!(battery_status->PowerState & BATTERY_POWER_ON_LINE),
         .current_capacity = battery_status->Capacity,
         .full_charged_capacity = battery_information->FullChargedCapacity,
         .charge_unit =
             ((battery_information->Capabilities & BATTERY_CAPACITY_RELATIVE)
                  ? BatteryLevelUnit::kRelative
                  : BatteryLevelUnit::kMWh),
         .battery_discharge_granularity = battery_discharge_granularity}));
  }

  return MakeBatteryState(battery_details_list);
}

}  // namespace base
