// 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 "components/power_metrics/energy_impact_mac.h"

#include <Foundation/Foundation.h>
#import <IOKit/IOKitLib.h>

#include "base/apple/foundation_util.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/compiler_specific.h"
#include "base/mac/scoped_ioobject.h"
#include "base/strings/sys_string_conversions.h"
#include "base/time/time.h"
#include "components/power_metrics/mach_time_mac.h"
#include "components/power_metrics/resource_coalition_mac.h"

namespace power_metrics {

namespace {

NSDictionary* MaybeGetDictionaryFromPath(const base::FilePath& path) {
  // The folder where the energy coefficient plist files are stored.
  return [NSDictionary
      dictionaryWithContentsOfURL:base::apple::FilePathToNSURL(path)
                            error:nil];
}

double GetNamedCoefficientOrZero(NSDictionary* dict, NSString* key) {
  NSNumber* num = base::apple::ObjCCast<NSNumber>(dict[key]);
  return num.floatValue;
}

}  // namespace

std::optional<EnergyImpactCoefficients>
ReadCoefficientsForCurrentMachineOrDefault() {
  std::optional<std::string> board_id = internal::GetBoardIdForThisMachine();
  if (!board_id.has_value())
    return std::nullopt;

  return internal::ReadCoefficientsForBoardIdOrDefault(
      base::FilePath(FILE_PATH_LITERAL("/usr/share/pmenergy")),
      board_id.value());
}

double ComputeEnergyImpactForResourceUsage(
    const coalition_resource_usage& data_sample,
    const EnergyImpactCoefficients& coefficients,
    const mach_timebase_info_data_t& mach_timebase) {
  // TODO(crbug.com/40197639): The below coefficients are not used
  // for now. Their units are unknown, and in the case of the network-related
  // coefficients, it's not clear how to sample the data.
  //
  // coefficients.kdiskio_bytesread;
  // coefficients.kdiskio_byteswritten;
  // coefficients.knetwork_recv_bytes;
  // coefficients.knetwork_recv_packets;
  // coefficients.knetwork_sent_bytes;
  // coefficients.knetwork_sent_packets;

  // The cumulative CPU usage in |data_sample| is in units of ns, and
  // |cpu_time_equivalent_ns| is computed in CPU ns up to the end of this
  // function, where it's converted to units of EnergyImpact.
  double cpu_time_equivalent_ns = 0.0;

  // The kcpu_wakeups coefficient on disk is in seconds, but our
  // intermediate result is in ns, so convert to ns on the fly.
  cpu_time_equivalent_ns += coefficients.kcpu_wakeups *
                            base::Time::kNanosecondsPerSecond *
                            data_sample.platform_idle_wakeups;

  // Presumably the kgpu_time coefficient has suitable units for the
  // conversion of GPU time energy to CPU time energy. There is a fairly
  // wide spread on this constant seen in /usr/share/pmenergy. On
  // macOS 11.5.2 the spread is from 0 through 5.9.
  cpu_time_equivalent_ns += coefficients.kgpu_time *
                            MachTimeToNs(data_sample.gpu_time, mach_timebase);

  cpu_time_equivalent_ns +=
      coefficients.kqos_background *
      MachTimeToNs(
          UNSAFE_TODO(data_sample.cpu_time_eqos[THREAD_QOS_BACKGROUND]),
          mach_timebase);
  cpu_time_equivalent_ns +=
      coefficients.kqos_default *
      MachTimeToNs(UNSAFE_TODO(data_sample.cpu_time_eqos[THREAD_QOS_DEFAULT]),
                   mach_timebase);
  cpu_time_equivalent_ns +=
      coefficients.kqos_legacy *
      MachTimeToNs(UNSAFE_TODO(data_sample.cpu_time_eqos[THREAD_QOS_LEGACY]),
                   mach_timebase);
  cpu_time_equivalent_ns +=
      coefficients.kqos_user_initiated *
      MachTimeToNs(
          UNSAFE_TODO(data_sample.cpu_time_eqos[THREAD_QOS_USER_INITIATED]),
          mach_timebase);
  cpu_time_equivalent_ns +=
      coefficients.kqos_user_interactive *
      MachTimeToNs(
          UNSAFE_TODO(data_sample.cpu_time_eqos[THREAD_QOS_USER_INTERACTIVE]),
          mach_timebase);
  cpu_time_equivalent_ns +=
      coefficients.kqos_utility *
      MachTimeToNs(UNSAFE_TODO(data_sample.cpu_time_eqos[THREAD_QOS_UTILITY]),
                   mach_timebase);

  // The conversion ratio for CPU time/EnergyImpact is ns/10ms
  constexpr double kNsToEI = 1E-7;
  return cpu_time_equivalent_ns * kNsToEI;
}

namespace internal {

std::optional<EnergyImpactCoefficients> ReadCoefficientsFromPath(
    const base::FilePath& plist_file) {
  @autoreleasepool {
    NSDictionary* dict = MaybeGetDictionaryFromPath(plist_file);
    if (!dict) {
      return std::nullopt;
    }

    NSDictionary* energy_constants = dict[@"energy_constants"];
    if (!energy_constants) {
      return std::nullopt;
    }

    EnergyImpactCoefficients coefficients{};
    coefficients.kcpu_time =
        GetNamedCoefficientOrZero(energy_constants, @"kcpu_time");
    coefficients.kcpu_wakeups =
        GetNamedCoefficientOrZero(energy_constants, @"kcpu_wakeups");

    coefficients.kqos_default =
        GetNamedCoefficientOrZero(energy_constants, @"kqos_default");
    coefficients.kqos_background =
        GetNamedCoefficientOrZero(energy_constants, @"kqos_background");
    coefficients.kqos_utility =
        GetNamedCoefficientOrZero(energy_constants, @"kqos_utility");
    coefficients.kqos_legacy =
        GetNamedCoefficientOrZero(energy_constants, @"kqos_legacy");
    coefficients.kqos_user_initiated =
        GetNamedCoefficientOrZero(energy_constants, @"kqos_user_initiated");
    coefficients.kqos_user_interactive =
        GetNamedCoefficientOrZero(energy_constants, @"kqos_user_interactive");

    coefficients.kdiskio_bytesread =
        GetNamedCoefficientOrZero(energy_constants, @"kdiskio_bytesread");
    coefficients.kdiskio_byteswritten =
        GetNamedCoefficientOrZero(energy_constants, @"kdiskio_byteswritten");

    coefficients.kgpu_time =
        GetNamedCoefficientOrZero(energy_constants, @"kgpu_time");

    coefficients.knetwork_recv_bytes =
        GetNamedCoefficientOrZero(energy_constants, @"knetwork_recv_bytes");
    coefficients.knetwork_recv_packets =
        GetNamedCoefficientOrZero(energy_constants, @"knetwork_recv_packets");
    coefficients.knetwork_sent_bytes =
        GetNamedCoefficientOrZero(energy_constants, @"knetwork_sent_bytes");
    coefficients.knetwork_sent_packets =
        GetNamedCoefficientOrZero(energy_constants, @"knetwork_sent_packets");

    return coefficients;
  }
}

std::optional<EnergyImpactCoefficients> ReadCoefficientsForBoardIdOrDefault(
    const base::FilePath& directory,
    const std::string& board_id) {
  auto coefficients = ReadCoefficientsFromPath(
      directory.Append(board_id).AddExtension(FILE_PATH_LITERAL("plist")));
  if (coefficients.has_value())
    return coefficients;

  return ReadCoefficientsFromPath(
      directory.Append(FILE_PATH_LITERAL("default.plist")));
}

std::optional<std::string> GetBoardIdForThisMachine() {
  base::mac::ScopedIOObject<io_service_t> platform_expert(
      IOServiceGetMatchingService(kIOMainPortDefault,
                                  IOServiceMatching("IOPlatformExpertDevice")));
  if (!platform_expert)
    return std::nullopt;

  // This is what libpmenergy is observed to do in order to retrieve the correct
  // coefficients file for the local computer.
  base::apple::ScopedCFTypeRef<CFDataRef> board_id_data(
      base::apple::CFCast<CFDataRef>(IORegistryEntryCreateCFProperty(
          platform_expert.get(), CFSTR("board-id"), kCFAllocatorDefault, 0)));

  if (!board_id_data)
    return std::nullopt;

  return reinterpret_cast<const char*>(CFDataGetBytePtr(board_id_data.get()));
}

}  // namespace internal

}  // namespace power_metrics
