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

#include "remoting/host/resizing_host_observer.h"

#include <stdint.h>

#include <algorithm>
#include <list>
#include <utility>

#include "base/check.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/time/default_tick_clock.h"
#include "base/time/tick_clock.h"
#include "remoting/base/logging.h"
#include "remoting/host/base/screen_resolution.h"
#include "remoting/host/desktop_display_info_monitor.h"
#include "remoting/host/desktop_resizer.h"
#include "remoting/proto/control.pb.h"

namespace remoting {
namespace {

// Minimum amount of time to wait between desktop resizes. Note that this
// constant is duplicated by the ResizingHostObserverTest.RateLimited
// unit-test and must be kept in sync.
const int kMinimumResizeIntervalMs = 1000;

class CandidateResolution {
 public:
  CandidateResolution(const ScreenResolution& candidate,
                      const ScreenResolution& preferred)
      : resolution_(candidate) {
    // Protect against division by zero.
    CHECK(!candidate.IsEmpty());
    DCHECK(!preferred.IsEmpty());

    // The client scale factor is the smaller of the candidate:preferred ratios
    // for width and height.
    if ((candidate.dimensions().width() > preferred.dimensions().width()) ||
        (candidate.dimensions().height() > preferred.dimensions().height())) {
      const float width_ratio =
          static_cast<float>(preferred.dimensions().width()) /
          candidate.dimensions().width();
      const float height_ratio =
          static_cast<float>(preferred.dimensions().height()) /
          candidate.dimensions().height();
      client_scale_factor_ = std::min(width_ratio, height_ratio);
    } else {
      // Since clients do not scale up, 1.0 is the maximum.
      client_scale_factor_ = 1.0;
    }

    // The aspect ratio "goodness" is defined as being the ratio of the smaller
    // of the two aspect ratios (candidate and preferred) to the larger. The
    // best aspect ratio is the one that most closely matches the preferred
    // aspect ratio (in other words, the ideal aspect ratio "goodness" is 1.0).
    // By keeping the values < 1.0, it allows ratios that differ in opposite
    // directions to be compared numerically.
    float candidate_aspect_ratio =
        static_cast<float>(candidate.dimensions().width()) /
        candidate.dimensions().height();
    float preferred_aspect_ratio =
        static_cast<float>(preferred.dimensions().width()) /
        preferred.dimensions().height();
    if (candidate_aspect_ratio > preferred_aspect_ratio) {
      aspect_ratio_goodness_ = preferred_aspect_ratio / candidate_aspect_ratio;
    } else {
      aspect_ratio_goodness_ = candidate_aspect_ratio / preferred_aspect_ratio;
    }
  }

  const ScreenResolution& resolution() const { return resolution_; }
  float client_scale_factor() const { return client_scale_factor_; }
  float aspect_ratio_goodness() const { return aspect_ratio_goodness_; }
  int64_t area() const {
    return static_cast<int64_t>(resolution_.dimensions().width()) *
           resolution_.dimensions().height();
  }

  // TODO(jamiewalch): Also compare the DPI: http://crbug.com/172405
  bool IsBetterThan(const CandidateResolution& other) const {
    // If either resolution would require down-scaling, prefer the one that
    // down-scales the least (since the client scale factor is at most 1.0,
    // this does not differentiate between resolutions that don't require
    // down-scaling).
    if (client_scale_factor() < other.client_scale_factor()) {
      return false;
    } else if (client_scale_factor() > other.client_scale_factor()) {
      return true;
    }

    // If the scale factors are the same, pick the resolution with the largest
    // area.
    if (area() < other.area()) {
      return false;
    } else if (area() > other.area()) {
      return true;
    }

    // If the areas are equal, pick the resolution with the "best" aspect ratio.
    if (aspect_ratio_goodness() < other.aspect_ratio_goodness()) {
      return false;
    } else if (aspect_ratio_goodness() > other.aspect_ratio_goodness()) {
      return true;
    }

    // All else being equal (for example, comparing 640x480 to 480x640 w.r.t.
    // 640x640), just pick the widest, since desktop UIs are typically designed
    // for landscape aspect ratios.
    return resolution().dimensions().width() >
           other.resolution().dimensions().width();
  }

 private:
  float client_scale_factor_;
  float aspect_ratio_goodness_;
  ScreenResolution resolution_;
};

}  // namespace

ResizingHostObserver::ResizingHostObserver(
    std::unique_ptr<DesktopResizer> desktop_resizer,
    bool restore)
    : desktop_resizer_(std::move(desktop_resizer)),
      restore_(restore),
      clock_(base::DefaultTickClock::GetInstance()) {}

ResizingHostObserver::~ResizingHostObserver() {
  if (restore_) {
    RestoreAllScreenResolutions();
  }
}

void ResizingHostObserver::RegisterForDisplayChanges(
    DesktopDisplayInfoMonitor& monitor) {
  monitor.AddCallback(base::BindRepeating(
      &ResizingHostObserver::OnDisplayInfoChanged, weak_factory_.GetWeakPtr()));
}

void ResizingHostObserver::SetScreenResolution(
    const ScreenResolution& resolution,
    std::optional<webrtc::ScreenId> opt_screen_id) {
  // Get the current time. This function is called exactly once for each call
  // to SetScreenResolution to simplify the implementation of unit-tests.
  base::TimeTicks now = clock_->NowTicks();

  webrtc::ScreenId screen_id;
  if (opt_screen_id) {
    screen_id = opt_screen_id.value();
  } else {
    // If SetScreenResolution() was called without any ID, the ID of the
    // single monitor should be used. If there are no monitors yet, the request
    // is remembered, to be applied when the display-info is updated.
    if (current_monitor_ids_.empty()) {
      pending_resolution_request_ = resolution;
      return;
    }
    if (current_monitor_ids_.size() == 1) {
      screen_id = *current_monitor_ids_.begin();
    } else {
      // Drop the request if there is more than 1 monitor.
      HOST_LOG << "Ignoring ambiguous resize request.";
      return;
    }
  }

  // Drop any request for an invalid screen ID.
  if (!base::Contains(current_monitor_ids_, screen_id)) {
    HOST_LOG << "Ignoring resize request for invalid monitor ID " << screen_id
             << ".";
    return;
  }

  if (resolution.IsEmpty()) {
    RestoreScreenResolution(screen_id);
    return;
  }

  // Resizing the desktop too often is probably not a good idea, so apply a
  // simple rate-limiting scheme.
  // TODO(crbug.com/40225767): Rate-limiting should only be applied to requests
  // for the same monitor.
  base::TimeTicks next_allowed_resize =
      previous_resize_time_ + base::Milliseconds(kMinimumResizeIntervalMs);

  if (now < next_allowed_resize) {
    deferred_resize_timer_.Start(
        FROM_HERE, next_allowed_resize - now,
        base::BindOnce(&ResizingHostObserver::SetScreenResolution,
                       weak_factory_.GetWeakPtr(), resolution, opt_screen_id));
    return;
  }

  // If the implementation returns any resolutions, pick the best one according
  // to the algorithm described in CandidateResolution::IsBetterThan.
  std::list<ScreenResolution> resolutions =
      desktop_resizer_->GetSupportedResolutions(resolution, screen_id);
  if (resolutions.empty()) {
    HOST_LOG << "No valid resolutions found for monitor ID " << screen_id
             << ".";
    return;
  } else {
    HOST_LOG << "Found host resolutions for monitor ID " << screen_id << ":";
    for (const auto& host_resolution : resolutions) {
      HOST_LOG << "  " << host_resolution.dimensions().width() << "x"
               << host_resolution.dimensions().height();
    }
  }
  HOST_LOG << "Choosing best candidate for client resolution: "
           << resolution.dimensions().width() << "x"
           << resolution.dimensions().height() << " [" << resolution.dpi().x()
           << ", " << resolution.dpi().y() << "]";
  CandidateResolution best_candidate(resolutions.front(), resolution);
  for (std::list<ScreenResolution>::const_iterator i = ++resolutions.begin();
       i != resolutions.end(); ++i) {
    CandidateResolution candidate(*i, resolution);
    if (candidate.IsBetterThan(best_candidate)) {
      best_candidate = candidate;
    }
  }
  ScreenResolution current_resolution =
      desktop_resizer_->GetCurrentResolution(screen_id);
  ScreenResolution best_resolution = best_candidate.resolution();

  if (!best_resolution.Equals(current_resolution)) {
    RecordOriginalResolution(current_resolution, screen_id);
    HOST_LOG << "Resizing monitor ID " << screen_id << " to "
             << best_resolution.dimensions().width() << "x"
             << best_resolution.dimensions().height() << " ["
             << best_resolution.dpi().x() << ", " << best_resolution.dpi().y()
             << "].";
    desktop_resizer_->SetResolution(best_resolution, screen_id);
  } else {
    HOST_LOG << "Not resizing monitor ID " << screen_id
             << "; desktop dimensions already "
             << best_resolution.dimensions().width() << "x"
             << best_resolution.dimensions().height() << " ["
             << best_resolution.dpi().x() << ", " << best_resolution.dpi().y()
             << "].";
  }

  // Update the time of last resize to allow it to be rate-limited.
  previous_resize_time_ = now;
}

void ResizingHostObserver::SetVideoLayout(
    const protocol::VideoLayout& video_layout) {
  desktop_resizer_->SetVideoLayout(video_layout);
}

void ResizingHostObserver::SetDisplayInfoForTesting(
    const DesktopDisplayInfo& display_info) {
  OnDisplayInfoChanged(display_info);
}

void ResizingHostObserver::SetClockForTesting(const base::TickClock* clock) {
  clock_ = clock;
}

void ResizingHostObserver::RestoreScreenResolution(webrtc::ScreenId screen_id) {
  auto iter = original_resolutions_.find(screen_id);
  if (iter != original_resolutions_.end()) {
    auto [_, original_resolution] = *iter;
    HOST_LOG << "Restoring monitor ID " << screen_id << " to "
             << original_resolution.dimensions().width() << "x"
             << original_resolution.dimensions().height() << ".";
    desktop_resizer_->RestoreResolution(original_resolution, screen_id);
    original_resolutions_.erase(iter);
  } else {
    HOST_LOG << "No original resolution found for monitor ID " << screen_id
             << ".";
  }
}

void ResizingHostObserver::RestoreAllScreenResolutions() {
  while (!original_resolutions_.empty()) {
    auto [screen_id, _] = *original_resolutions_.begin();
    RestoreScreenResolution(screen_id);
  }
}

void ResizingHostObserver::RecordOriginalResolution(
    ScreenResolution resolution,
    webrtc::ScreenId screen_id) {
  if (!base::Contains(original_resolutions_, screen_id)) {
    original_resolutions_[screen_id] = resolution;
  }
}

void ResizingHostObserver::OnDisplayInfoChanged(
    const DesktopDisplayInfo& display_info) {
  current_monitor_ids_.clear();
  for (int i = 0; i < display_info.NumDisplays(); i++) {
    current_monitor_ids_.insert(display_info.GetDisplayInfo(i)->id);
  }

  // If there was a pending resolution request for an unspecifed monitor, apply
  // it now.
  if (!pending_resolution_request_.IsEmpty()) {
    SetScreenResolution(pending_resolution_request_, std::nullopt);
    pending_resolution_request_ = {};
  }
}

}  // namespace remoting