// 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 "components/arc/power/arc_power_bridge.h"

#include <algorithm>
#include <utility>

#include "ash/shell.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/singleton.h"
#include "chromeos/dbus/dbus_thread_manager.h"
#include "chromeos/dbus/power_manager/backlight.pb.h"
#include "chromeos/dbus/power_policy_controller.h"
#include "components/arc/arc_bridge_service.h"
#include "components/arc/arc_browser_context_keyed_service_factory_base.h"
#include "components/arc/arc_service_manager.h"
#include "content/public/common/service_manager_connection.h"
#include "services/device/public/mojom/constants.mojom.h"
#include "services/device/public/mojom/wake_lock.mojom.h"
#include "services/device/public/mojom/wake_lock_provider.mojom.h"
#include "services/service_manager/public/cpp/connector.h"

namespace arc {
namespace {

// Delay for notifying Android about screen brightness changes, added in
// order to prevent spammy brightness updates.
constexpr base::TimeDelta kNotifyBrightnessDelay =
    base::TimeDelta::FromMilliseconds(200);

// Singleton factory for ArcPowerBridge.
class ArcPowerBridgeFactory
    : public internal::ArcBrowserContextKeyedServiceFactoryBase<
          ArcPowerBridge,
          ArcPowerBridgeFactory> {
 public:
  // Factory name used by ArcBrowserContextKeyedServiceFactoryBase.
  static constexpr const char* kName = "ArcPowerBridgeFactory";

  static ArcPowerBridgeFactory* GetInstance() {
    return base::Singleton<ArcPowerBridgeFactory>::get();
  }

 private:
  friend base::DefaultSingletonTraits<ArcPowerBridgeFactory>;
  ArcPowerBridgeFactory() = default;
  ~ArcPowerBridgeFactory() override = default;
};

}  // namespace

// WakeLockRequestor requests a wake lock from the device service in response
// to wake lock requests of a given type from Android. A count is kept of
// outstanding Android requests so that only a single actual wake lock is used.
class ArcPowerBridge::WakeLockRequestor {
 public:
  WakeLockRequestor(device::mojom::WakeLockType type,
                    service_manager::Connector* connector)
      : type_(type), connector_(connector) {}
  ~WakeLockRequestor() = default;

  // Increments the number of outstanding requests from Android and requests a
  // wake lock from the device service if this is the only request.
  void AddRequest() {
    num_android_requests_++;
    if (num_android_requests_ > 1)
      return;

    // Initialize |wake_lock_| if this is the first time we're using it.
    if (!wake_lock_) {
      device::mojom::WakeLockProviderPtr provider;
      connector_->BindInterface(device::mojom::kServiceName,
                                mojo::MakeRequest(&provider));
      provider->GetWakeLockWithoutContext(
          type_, device::mojom::WakeLockReason::kOther, "ARC",
          mojo::MakeRequest(&wake_lock_));
    }

    wake_lock_->RequestWakeLock();
  }

  // Decrements the number of outstanding Android requests. Cancels the device
  // service wake lock when the request count hits zero.
  void RemoveRequest() {
    DCHECK_GT(num_android_requests_, 0);
    num_android_requests_--;
    if (num_android_requests_ >= 1)
      return;

    DCHECK(wake_lock_);
    wake_lock_->CancelWakeLock();
  }

  // Runs the message loop until replies have been received for all pending
  // requests on |wake_lock_|.
  void FlushForTesting() {
    if (wake_lock_)
      wake_lock_.FlushForTesting();
  }

 private:
  // Type of wake lock to request.
  device::mojom::WakeLockType type_;

  // Used to get services. Not owned.
  service_manager::Connector* const connector_ = nullptr;

  // Number of outstanding Android requests.
  int num_android_requests_ = 0;

  // Lazily initialized in response to first request.
  device::mojom::WakeLockPtr wake_lock_;

  DISALLOW_COPY_AND_ASSIGN(WakeLockRequestor);
};

// static
ArcPowerBridge* ArcPowerBridge::GetForBrowserContext(
    content::BrowserContext* context) {
  return ArcPowerBridgeFactory::GetForBrowserContext(context);
}

ArcPowerBridge::ArcPowerBridge(content::BrowserContext* context,
                               ArcBridgeService* bridge_service)
    : arc_bridge_service_(bridge_service),
      weak_ptr_factory_(this) {
  arc_bridge_service_->power()->SetHost(this);
  arc_bridge_service_->power()->AddObserver(this);
}

ArcPowerBridge::~ArcPowerBridge() {
  arc_bridge_service_->power()->RemoveObserver(this);
  arc_bridge_service_->power()->SetHost(nullptr);
}

bool ArcPowerBridge::TriggerNotifyBrightnessTimerForTesting() {
  if (!notify_brightness_timer_.IsRunning())
    return false;
  notify_brightness_timer_.FireNow();
  return true;
}

void ArcPowerBridge::FlushWakeLocksForTesting() {
  for (const auto& it : wake_lock_requestors_)
    it.second->FlushForTesting();
}

void ArcPowerBridge::OnConnectionReady() {
  // TODO(mash): Support this functionality without ash::Shell access in Chrome.
  if (ash::Shell::HasInstance())
    ash::Shell::Get()->display_configurator()->AddObserver(this);
  chromeos::DBusThreadManager::Get()->GetPowerManagerClient()->
      AddObserver(this);
  chromeos::DBusThreadManager::Get()
      ->GetPowerManagerClient()
      ->GetScreenBrightnessPercent(
          base::BindOnce(&ArcPowerBridge::OnGetScreenBrightnessPercent,
                         weak_ptr_factory_.GetWeakPtr()));
}

void ArcPowerBridge::OnConnectionClosed() {
  // TODO(mash): Support this functionality without ash::Shell access in Chrome.
  if (ash::Shell::HasInstance())
    ash::Shell::Get()->display_configurator()->RemoveObserver(this);
  chromeos::DBusThreadManager::Get()->GetPowerManagerClient()->
      RemoveObserver(this);
  wake_lock_requestors_.clear();
}

void ArcPowerBridge::SuspendImminent(
    power_manager::SuspendImminent::Reason reason) {
  mojom::PowerInstance* power_instance =
      ARC_GET_INSTANCE_FOR_METHOD(arc_bridge_service_->power(), Suspend);
  if (!power_instance)
    return;

  power_instance->Suspend(chromeos::DBusThreadManager::Get()
                              ->GetPowerManagerClient()
                              ->GetSuspendReadinessCallback(FROM_HERE));
}

void ArcPowerBridge::SuspendDone(const base::TimeDelta& sleep_duration) {
  mojom::PowerInstance* power_instance =
      ARC_GET_INSTANCE_FOR_METHOD(arc_bridge_service_->power(), Resume);
  if (!power_instance)
    return;

  power_instance->Resume();
}

void ArcPowerBridge::ScreenBrightnessChanged(
    const power_manager::BacklightBrightnessChange& change) {
  const base::TimeTicks now = base::TimeTicks::Now();
  if (last_brightness_changed_time_.is_null() ||
      (now - last_brightness_changed_time_) >= kNotifyBrightnessDelay) {
    UpdateAndroidScreenBrightness(change.percent());
    notify_brightness_timer_.Stop();
  } else {
    notify_brightness_timer_.Start(
        FROM_HERE, kNotifyBrightnessDelay,
        base::Bind(&ArcPowerBridge::UpdateAndroidScreenBrightness,
                   weak_ptr_factory_.GetWeakPtr(), change.percent()));
  }
  last_brightness_changed_time_ = now;
}

void ArcPowerBridge::OnPowerStateChanged(
    chromeos::DisplayPowerState power_state) {
  mojom::PowerInstance* power_instance =
      ARC_GET_INSTANCE_FOR_METHOD(arc_bridge_service_->power(), SetInteractive);
  if (!power_instance)
    return;

  bool enabled = (power_state != chromeos::DISPLAY_POWER_ALL_OFF);
  power_instance->SetInteractive(enabled);
}

void ArcPowerBridge::OnAcquireDisplayWakeLock(mojom::DisplayWakeLockType type) {
  switch (type) {
    case mojom::DisplayWakeLockType::BRIGHT:
      GetWakeLockRequestor(device::mojom::WakeLockType::kPreventDisplaySleep)
          ->AddRequest();
      break;
    case mojom::DisplayWakeLockType::DIM:
      GetWakeLockRequestor(
          device::mojom::WakeLockType::kPreventDisplaySleepAllowDimming)
          ->AddRequest();
      break;
    default:
      LOG(WARNING) << "Tried to take invalid wake lock type "
                   << static_cast<int>(type);
      return;
  }
}

void ArcPowerBridge::OnReleaseDisplayWakeLock(mojom::DisplayWakeLockType type) {
  switch (type) {
    case mojom::DisplayWakeLockType::BRIGHT:
      GetWakeLockRequestor(device::mojom::WakeLockType::kPreventDisplaySleep)
          ->RemoveRequest();
      break;
    case mojom::DisplayWakeLockType::DIM:
      GetWakeLockRequestor(
          device::mojom::WakeLockType::kPreventDisplaySleepAllowDimming)
          ->RemoveRequest();
      break;
    default:
      LOG(WARNING) << "Tried to take invalid wake lock type "
                   << static_cast<int>(type);
      return;
  }
}

void ArcPowerBridge::IsDisplayOn(IsDisplayOnCallback callback) {
  bool is_display_on = false;
  // TODO(mash): Support this functionality without ash::Shell access in Chrome.
  if (ash::Shell::HasInstance())
    is_display_on = ash::Shell::Get()->display_configurator()->IsDisplayOn();
  std::move(callback).Run(is_display_on);
}

void ArcPowerBridge::OnScreenBrightnessUpdateRequest(double percent) {
  chromeos::DBusThreadManager::Get()
      ->GetPowerManagerClient()
      ->SetScreenBrightnessPercent(percent, true);
}

ArcPowerBridge::WakeLockRequestor* ArcPowerBridge::GetWakeLockRequestor(
    device::mojom::WakeLockType type) {
  auto it = wake_lock_requestors_.find(type);
  if (it != wake_lock_requestors_.end())
    return it->second.get();

  service_manager::Connector* connector =
      connector_for_test_
          ? connector_for_test_
          : content::ServiceManagerConnection::GetForProcess()->GetConnector();
  DCHECK(connector);

  it = wake_lock_requestors_
           .emplace(type, std::make_unique<WakeLockRequestor>(type, connector))
           .first;
  return it->second.get();
}

void ArcPowerBridge::OnGetScreenBrightnessPercent(
    base::Optional<double> percent) {
  if (!percent.has_value()) {
    LOG(ERROR)
        << "PowerManagerClient::GetScreenBrightnessPercent reports an error";
    return;
  }
  UpdateAndroidScreenBrightness(percent.value());
}

void ArcPowerBridge::UpdateAndroidScreenBrightness(double percent) {
  mojom::PowerInstance* power_instance = ARC_GET_INSTANCE_FOR_METHOD(
      arc_bridge_service_->power(), UpdateScreenBrightnessSettings);
  if (!power_instance)
    return;
  power_instance->UpdateScreenBrightnessSettings(percent);
}

}  // namespace arc
