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

#include "content/browser/renderer_host/browser_compositor_view_mac.h"

#import <Cocoa/Cocoa.h>
#include <stdint.h>

#include <memory>
#include <utility>

#include "base/command_line.h"
#include "base/containers/circular_deque.h"
#include "base/no_destructor.h"
#include "base/trace_event/trace_event.h"
#include "components/viz/common/features.h"
#include "components/viz/common/surfaces/local_surface_id.h"
#include "content/browser/compositor/image_transport_factory.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/context_factory.h"
#include "ui/accelerated_widget_mac/accelerated_widget_mac.h"
#include "ui/accelerated_widget_mac/window_resize_helper_mac.h"
#include "ui/compositor/recyclable_compositor_mac.h"
#include "ui/gfx/geometry/dip_util.h"
#include "ui/gfx/geometry/size_conversions.h"

namespace content {

namespace {

// Weak pointers to all BrowserCompositorMac instances, used to
// - Determine if a spare RecyclableCompositorMac should be kept around (one
//   should be only if there exists at least one BrowserCompositorMac).
// - Force all ui::Compositors to be destroyed at shut-down (because the NSView
//   signals to shut down will come in very late, long after things that the
//   ui::Compositor depend on have been destroyed).
//   https://crbug.com/805726
std::set<BrowserCompositorMac*>& GetBrowserCompositors() {
  static base::NoDestructor<std::set<BrowserCompositorMac*>> instance;
  return *instance.get();
}

}  // namespace

////////////////////////////////////////////////////////////////////////////////
// BrowserCompositorMac

BrowserCompositorMac::BrowserCompositorMac(
    ui::AcceleratedWidgetMacNSView* accelerated_widget_mac_ns_view,
    BrowserCompositorMacClient* client,
    bool render_widget_host_is_hidden,
    const viz::FrameSinkId& frame_sink_id)
    : client_(client),
      accelerated_widget_mac_ns_view_(accelerated_widget_mac_ns_view),
      weak_factory_(this) {
  GetBrowserCompositors().insert(this);

  root_layer_ = std::make_unique<ui::LayerSurface>();
  // Ensure that this layer draws nothing when it does not not have delegated
  // content (otherwise this solid color will be flashed during navigation).
  root_layer_->SetFallbackBackgroundColor(SkColors::kTransparent);
  root_layer_->SetFillsBoundsOpaquely(false);
  delegated_frame_host_ = std::make_unique<DelegatedFrameHost>(
      frame_sink_id, this, true /* should_register_frame_sink_id */);

  SetRenderWidgetHostIsHidden(render_widget_host_is_hidden);
}

BrowserCompositorMac::~BrowserCompositorMac() {
  // Ensure that copy callbacks completed or cancelled during further tear-down
  // do not call back into this.
  weak_factory_.InvalidateWeakPtrs();

  TransitionToState(HasNoCompositor);
  delegated_frame_host_.reset();
  root_layer_.reset();

  size_t num_erased = GetBrowserCompositors().erase(this);
  CHECK_EQ(1u, num_erased, base::NotFatalUntil::M152);
}

DelegatedFrameHost* BrowserCompositorMac::GetDelegatedFrameHost() {
  CHECK(delegated_frame_host_, base::NotFatalUntil::M152);
  return delegated_frame_host_.get();
}

bool BrowserCompositorMac::ForceNewSurfaceId() {
  dfh_local_surface_id_allocator_.GenerateId();
  delegated_frame_host_->EmbedSurface(
      dfh_local_surface_id_allocator_.GetCurrentLocalSurfaceId(), dfh_size_dip_,
      cc::DeadlinePolicy::UseExistingDeadline());
  return client_->OnBrowserCompositorSurfaceIdChanged();
}

const gfx::CALayerParams* BrowserCompositorMac::GetLastCALayerParams() const {
  if (!recyclable_compositor_)
    return nullptr;
  return recyclable_compositor_->widget()->GetCALayerParams();
}

viz::FrameSinkId BrowserCompositorMac::GetRootFrameSinkId() {
  if (parent_ui_layer_)
    return parent_ui_layer_->GetCompositor()->frame_sink_id();
  if (recyclable_compositor_)
    return recyclable_compositor_->compositor()->frame_sink_id();
  return viz::FrameSinkId();
}

void BrowserCompositorMac::SetBackgroundColor(SkColor background_color) {
  background_color_ = background_color;
  if (recyclable_compositor_)
    recyclable_compositor_->compositor()->SetBackgroundColor(background_color_);
}

void BrowserCompositorMac::UpdateSurfaceFromNSView(
    const gfx::Size& new_size_dip) {
  display::ScreenInfo current = client_->GetCurrentScreenInfo();

  bool needs_new_surface_id =
      new_size_dip != dfh_size_dip_ ||
      current.device_scale_factor != dfh_device_scale_factor_;

  dfh_size_dip_ = new_size_dip;
  dfh_device_scale_factor_ = current.device_scale_factor;

  // The device scale factor is always an integer, so the result here is also
  // an integer.
  dfh_size_pixels_ = gfx::ToRoundedSize(
      gfx::ConvertSizeToPixels(dfh_size_dip_, current.device_scale_factor));
  root_layer_->SetBounds(gfx::Rect(dfh_size_dip_));

  if (needs_new_surface_id) {
    dfh_local_surface_id_allocator_.GenerateId();
    delegated_frame_host_->EmbedSurface(
        dfh_local_surface_id_allocator_.GetCurrentLocalSurfaceId(),
        dfh_size_dip_, cc::DeadlinePolicy::UseSpecifiedDeadline(0u));
  }

  if (recyclable_compositor_) {
    recyclable_compositor_->UpdateSurface(
        dfh_size_pixels_, current.device_scale_factor,
        current.display_color_spaces, current.display_id,
        current.display_frequency);
  }
}

void BrowserCompositorMac::UpdateSurfaceFromChild(
    bool auto_resize_enabled,
    float new_device_scale_factor,
    const gfx::Size& new_size_in_pixels,
    const viz::LocalSurfaceId& child_local_surface_id) {
  if (dfh_local_surface_id_allocator_.UpdateFromChild(child_local_surface_id)) {
    if (auto_resize_enabled) {
      client_->SetCurrentDeviceScaleFactor(new_device_scale_factor);
      display::ScreenInfo current = client_->GetCurrentScreenInfo();
      // TODO(danakj): We should avoid lossy conversions to integer DIPs.
      dfh_size_dip_ = gfx::ToFlooredSize(gfx::ConvertSizeToDips(
          new_size_in_pixels, current.device_scale_factor));
      dfh_size_pixels_ = new_size_in_pixels;
      dfh_device_scale_factor_ = new_device_scale_factor;
      root_layer_->SetBounds(gfx::Rect(dfh_size_dip_));
      if (recyclable_compositor_) {
        recyclable_compositor_->UpdateSurface(
            dfh_size_pixels_, current.device_scale_factor,
            current.display_color_spaces, current.display_id,
            current.display_frequency);
      }
    }
    delegated_frame_host_->EmbedSurface(
        dfh_local_surface_id_allocator_.GetCurrentLocalSurfaceId(),
        dfh_size_dip_, cc::DeadlinePolicy::UseDefaultDeadline());
  }
  client_->OnBrowserCompositorSurfaceIdChanged();
}

void BrowserCompositorMac::SetRenderWidgetHostIsHidden(bool hidden) {
  render_widget_host_is_hidden_ = hidden;
  UpdateState();
  if (state_ == UseParentLayerCompositor) {
    if (!hidden) {
      // UpdateState might not call WasShown when showing a frame using the same
      // ParentLayerCompositor, since it returns early on a no-op state
      // transition.
      delegated_frame_host_->WasShown(GetRendererLocalSurfaceId(),
                                      dfh_size_dip_,
                                      {} /* record_tab_switch_time_request */);
    } else {
      // A WebContents might be hidden without being detached from its
      // parent layer (e.g. Omnibox popup with DetachWebContentsOnHide
      // disabled). On Mac, when this happens, UpdateState transitions to
      // UseParentLayerCompositor which is a no-op, skipping WasHidden(). We
      // must explicitly call it here to ensure the frame is unlocked from the
      // compositor cache.
      if (base::FeatureList::IsEnabled(features::kHideDelegatedFrameHostMac)) {
        delegated_frame_host_->WasHidden(
            DelegatedFrameHost::HiddenCause::kOther);
      }
    }
  }
}

void BrowserCompositorMac::SetViewVisible(bool visible) {
  root_layer_->SetVisible(visible);
}

void BrowserCompositorMac::UpdateState() {
  // Always use the parent ui::Layer's ui::Compositor if available.
  if (parent_ui_layer_) {
    TransitionToState(UseParentLayerCompositor);
    return;
  }

  // If the host is visible and a compositor is required then create one.
  if (!render_widget_host_is_hidden_) {
    TransitionToState(HasOwnCompositor);
    return;
  }

  // Otherwise put the compositor up for recycling.
  TransitionToState(HasNoCompositor);
}

void BrowserCompositorMac::TransitionToState(State new_state) {
  // Skip if there is no change to make.
  bool is_no_op = false;
  if (state_ == new_state) {
    if (state_ == UseParentLayerCompositor)
      is_no_op = parent_ui_layer_ == root_layer_->parent();
    else
      is_no_op = true;
  }
  if (is_no_op)
    return;

  // First, detach from the current compositor, if there is one.
  delegated_frame_host_->DetachFromCompositor();
  if (state_ == UseParentLayerCompositor) {
    CHECK(root_layer_->parent(), base::NotFatalUntil::M152);
    state_ = HasNoCompositor;
    root_layer_->parent()->RemoveObserver(this);
    root_layer_->parent()->Remove(root_layer_.get());
  }
  if (state_ == HasOwnCompositor) {
    recyclable_compositor_->widget()->ResetNSView();
    recyclable_compositor_->compositor()->SetRootLayer(nullptr);
    recyclable_compositor_.reset();
  }

  // The compositor is now detached. If this is the target state, we're done.
  state_ = HasNoCompositor;
  if (new_state == HasNoCompositor) {
    // Don't transiently hide the DelegatedFrameHost because that can cause the
    // current frame to be inappropriately evicted.
    // https://crbug.com/897156
    delegated_frame_host_->WasHidden(DelegatedFrameHost::HiddenCause::kOther);
    return;
  }

  // Attach to the new compositor.
  if (new_state == UseParentLayerCompositor) {
    CHECK(parent_ui_layer_, base::NotFatalUntil::M152);
    parent_ui_layer_->Add(root_layer_.get());
    parent_ui_layer_->AddObserver(this);
    state_ = UseParentLayerCompositor;
  }
  if (new_state == HasOwnCompositor) {
    recyclable_compositor_ = std::make_unique<ui::RecyclableCompositorMac>(
        content::GetContextFactory());
    display::ScreenInfo current = client_->GetCurrentScreenInfo();
    recyclable_compositor_->UpdateSurface(
        dfh_size_pixels_, current.device_scale_factor,
        current.display_color_spaces, current.display_id,
        current.display_frequency);
    recyclable_compositor_->compositor()->SetRootLayer(root_layer_.get());
    recyclable_compositor_->compositor()->SetBackgroundColor(background_color_);
    recyclable_compositor_->widget()->SetNSView(
        accelerated_widget_mac_ns_view_);
    recyclable_compositor_->Unsuspend();
    state_ = HasOwnCompositor;
  }
  CHECK_EQ(state_, new_state, base::NotFatalUntil::M152);
  delegated_frame_host_->AttachToCompositor(GetCompositor());
  delegated_frame_host_->WasShown(GetRendererLocalSurfaceId(), dfh_size_dip_,
                                  {} /* record_tab_switch_time_request */);
}

// static
void BrowserCompositorMac::DisableRecyclingForShutdown() {
  // Ensure that the client has destroyed its BrowserCompositorViewMac before
  // it dependencies are destroyed.
  // https://crbug.com/805726
  while (!GetBrowserCompositors().empty()) {
    BrowserCompositorMac* browser_compositor = *GetBrowserCompositors().begin();
    browser_compositor->client_->DestroyCompositorForShutdown();
  }
}

void BrowserCompositorMac::TakeFallbackContentFrom(
    BrowserCompositorMac* other) {
  delegated_frame_host_->TakeFallbackContentFrom(
      other->delegated_frame_host_.get());
}

////////////////////////////////////////////////////////////////////////////////
// DelegatedFrameHost, public:

ui::LayerSurface* BrowserCompositorMac::GetDelegatedFrameHostLayer() const {
  return root_layer_.get();
}

bool BrowserCompositorMac::DelegatedFrameHostIsVisible() const {
  return state_ != HasNoCompositor;
}

SkColor BrowserCompositorMac::DelegatedFrameHostGetGutterColor() const {
  return client_->BrowserCompositorMacGetGutterColor();
}

void BrowserCompositorMac::OnFrameTokenChanged(
    uint32_t frame_token,
    base::TimeTicks activation_time) {
  client_->OnFrameTokenChanged(frame_token, activation_time);
}

float BrowserCompositorMac::GetDeviceScaleFactor() const {
  return dfh_device_scale_factor_;
}

void BrowserCompositorMac::InvalidateLocalSurfaceIdOnEviction() {
  dfh_local_surface_id_allocator_.Invalidate();
}

viz::FrameEvictorClient::EvictIds
BrowserCompositorMac::CollectSurfaceIdsForEviction() {
  viz::FrameEvictorClient::EvictIds ids;
  ids.embedded_ids = client_->CollectSurfaceIdsForEviction();
  return ids;
}

bool BrowserCompositorMac::ShouldShowStaleContentOnEviction() {
  return false;
}

cc::DeadlinePolicy BrowserCompositorMac::GetResizeDeadlinePolicy() const {
  // For remote windows (PWA app shim), use the default deadline to give the
  // renderer time to produce a correctly-sized frame. With deadline=0, the
  // compositor immediately shows stale content, which is highly visible
  // because the entire window area is web content.
  // https://crbug.com/493708175
  //
  // For in-process windows (regular browser tabs, content shell, popups),
  // use deadline=0 to produce new content as quickly as possible.
  if (client_->ShouldUseDefaultDeadlineOnResize()) {
    return cc::DeadlinePolicy::UseDefaultDeadline();
  }
  return cc::DeadlinePolicy::UseSpecifiedDeadline(0u);
}

void BrowserCompositorMac::DidNavigateMainFramePreCommit() {
  delegated_frame_host_->DidNavigateMainFramePreCommit();
}

void BrowserCompositorMac::DidEnterBackForwardCache() {
  dfh_local_surface_id_allocator_.GenerateId();
  delegated_frame_host_->DidEnterBackForwardCache();
}

void BrowserCompositorMac::ActivatedOrEvictedFromBackForwardCache() {
  delegated_frame_host_->ActivatedOrEvictedFromBackForwardCache();
}

void BrowserCompositorMac::DidNavigate() {
  if (render_widget_host_is_hidden_) {
    // Navigating while hidden should not allocate a new LocalSurfaceID. Once
    // sizes are ready, or we begin to Show, we can then allocate the new
    // LocalSurfaceId.
    dfh_local_surface_id_allocator_.Invalidate();
  } else {
    // The first navigation does not need a new LocalSurfaceID. The renderer can
    // use the ID that was already provided.
    if (!is_first_navigation_) {
      dfh_local_surface_id_allocator_.GenerateId();
    }
    delegated_frame_host_->EmbedSurface(
        dfh_local_surface_id_allocator_.GetCurrentLocalSurfaceId(),
        dfh_size_dip_, cc::DeadlinePolicy::UseExistingDeadline());
    client_->OnBrowserCompositorSurfaceIdChanged();
  }

  delegated_frame_host_->DidNavigate();
  is_first_navigation_ = false;
}

void BrowserCompositorMac::SetParentUiLayer(ui::Layer* new_parent_ui_layer) {
  if (new_parent_ui_layer)
    CHECK(new_parent_ui_layer->GetCompositor(), base::NotFatalUntil::M152);

  // Set |parent_ui_layer_| to the new value, which potentially not match the
  // value of |root_layer_->parent()|. The call to UpdateState will re-parent
  // |root_layer_|.
  CHECK_EQ(root_layer_->parent(), parent_ui_layer_, base::NotFatalUntil::M152);
  parent_ui_layer_ = new_parent_ui_layer;
  UpdateState();
  CHECK_EQ(root_layer_->parent(), parent_ui_layer_, base::NotFatalUntil::M152);
}

void BrowserCompositorMac::ForceNewSurfaceForTesting() {
  float current_device_scale_factor =
      client_->GetCurrentScreenInfo().device_scale_factor;
  client_->SetCurrentDeviceScaleFactor(current_device_scale_factor * 2.0f);
  UpdateSurfaceFromNSView(dfh_size_dip_);
}

viz::ScopedSurfaceIdAllocator
BrowserCompositorMac::GetScopedRendererSurfaceIdAllocator(
    base::OnceCallback<void()> allocation_task) {
  return viz::ScopedSurfaceIdAllocator(&dfh_local_surface_id_allocator_,
                                       std::move(allocation_task));
}

const viz::LocalSurfaceId& BrowserCompositorMac::GetRendererLocalSurfaceId() {
  if (!dfh_local_surface_id_allocator_.HasValidLocalSurfaceId())
    dfh_local_surface_id_allocator_.GenerateId();

  return dfh_local_surface_id_allocator_.GetCurrentLocalSurfaceId();
}

void BrowserCompositorMac::TransformPointToRootSurface(gfx::PointF* point) {
  gfx::Transform transform_to_root;
  if (parent_ui_layer_)
    parent_ui_layer_->GetTargetTransformRelativeTo(nullptr, &transform_to_root);
  *point = transform_to_root.MapPoint(*point);
}

void BrowserCompositorMac::LayerDestroyed(ui::Layer* layer) {
  CHECK_EQ(layer, parent_ui_layer_, base::NotFatalUntil::M152);
  SetParentUiLayer(nullptr);
}

ui::Compositor* BrowserCompositorMac::GetCompositor() const {
  if (parent_ui_layer_)
    return parent_ui_layer_->GetCompositor();
  if (recyclable_compositor_)
    return recyclable_compositor_->compositor();
  return nullptr;
}

void BrowserCompositorMac::InvalidateSurfaceAllocationGroup() {
  dfh_local_surface_id_allocator_.Invalidate(
      /*also_invalidate_allocation_group=*/true);
}

}  // namespace content
