// Copyright 2023 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_ios.h"

#include <stdint.h>

#include <memory>
#include <utility>

#include "base/command_line.h"
#include "base/containers/circular_deque.h"
#include "base/lazy_instance.h"
#include "base/task/single_thread_task_runner.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/browser/renderer_host/begin_frame_source_ios.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/context_factory.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/ui_base_features.h"
#include "ui/gfx/geometry/dip_util.h"
#include "ui/gfx/geometry/size_conversions.h"

namespace content {

////////////////////////////////////////////////////////////////////////////////
// BrowserCompositorIOS

BrowserCompositorIOS::BrowserCompositorIOS(
    gfx::AcceleratedWidget accelerated_widget,
    BrowserCompositorIOSClient* client,
    bool render_widget_host_is_hidden,
    const viz::FrameSinkId& frame_sink_id)
    : client_(client),
      accelerated_widget_(accelerated_widget),
      weak_factory_(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::kRed);
  root_layer_->SetFillsBoundsOpaquely(false);
  delegated_frame_host_ = std::make_unique<DelegatedFrameHost>(
      frame_sink_id, this, /*should_register_frame_sink_id=*/true);

  SetRenderWidgetHostIsHidden(render_widget_host_is_hidden);
}

BrowserCompositorIOS::~BrowserCompositorIOS() {
  // 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();
}

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

bool BrowserCompositorIOS::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();
}

viz::FrameSinkId BrowserCompositorIOS::GetRootFrameSinkId() {
  if (parent_ui_layer_) {
    return parent_ui_layer_->GetCompositor()->frame_sink_id();
  }
  if (compositor_) {
    return compositor_->frame_sink_id();
  }
  return viz::FrameSinkId();
}

void BrowserCompositorIOS::SetBackgroundColor(SkColor background_color) {
  // background_color_ = background_color;
  if (compositor_) {
    compositor_->SetBackgroundColor(background_color_);
  }
}

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

  bool is_resize = !dfh_size_dip_.IsEmpty() && new_size_dip != dfh_size_dip_;
  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_, GetDeadlinePolicy(is_resize));
  }

  if (compositor_) {
    UpdateSurface(dfh_size_pixels_, current.device_scale_factor,
                  current.display_color_spaces);
  }
}

void BrowserCompositorIOS::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 (compositor_) {
        UpdateSurface(dfh_size_pixels_, current.device_scale_factor,
                      current.display_color_spaces);
      }
    }
    delegated_frame_host_->EmbedSurface(
        dfh_local_surface_id_allocator_.GetCurrentLocalSurfaceId(),
        dfh_size_dip_, GetDeadlinePolicy(/*is_resize=*/true));
  }
  client_->OnBrowserCompositorSurfaceIdChanged();
}

void BrowserCompositorIOS::UpdateVSyncParameters(
    const base::TimeTicks& timebase,
    const base::TimeDelta& interval) {
  ui::Compositor* compositor = nullptr;
  if (compositor_) {
    compositor = compositor_.get();
  }
  // TODO(ccameron): VSync parameters for a ui::Compositor should be tracked
  // with the owner of that ui::Compositor (which, in the case of MacViews, is
  // BridgedNativeView). For the moment, push the VSync parameters from here to
  // the BridgedNativeView's ui::Compositor because that is a small change and
  // is easy to merge.
  // https://crbug.com/869129
  if (parent_ui_layer_) {
    compositor = parent_ui_layer_->GetCompositor();
  }
  if (compositor) {
    compositor->SetDisplayVSyncParameters(timebase, interval);
  }
}

void BrowserCompositorIOS::SetRenderWidgetHostIsHidden(bool hidden) {
  render_widget_host_is_hidden_ = hidden;
  UpdateState();
  if (state_ == UseParentLayerCompositor) {
    // 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=*/{});
  }
}

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

void BrowserCompositorIOS::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 BrowserCompositorIOS::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) {
    compositor_->SetRootLayer(nullptr);
    begin_frame_source_.reset();
    compositor_.reset();
    InvalidateSurface();
  }

  // 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) {
    ui::ContextFactory* context_factory = GetContextFactory();
    compositor_ = std::make_unique<ui::Compositor>(
        context_factory->AllocateFrameSinkId(), context_factory,
        base::SingleThreadTaskRunner::GetCurrentDefault(),
        features::IsPixelCanvasRecordingEnabled(),
        /*use_external_begin_frame_control=*/true);
    begin_frame_source_ =
        std::make_unique<BeginFrameSourceIOS>(compositor_.get());
    Suspend();
    display::ScreenInfo current = client_->GetCurrentScreenInfo();
    UpdateSurface(dfh_size_pixels_, current.device_scale_factor,
                  current.display_color_spaces);
    compositor_->SetRootLayer(root_layer_.get());
    compositor_->SetBackgroundColor(background_color_);
    compositor_->SetAcceleratedWidget(accelerated_widget_);
    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=*/{});
}

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

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

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

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

SkColor BrowserCompositorIOS::DelegatedFrameHostGetGutterColor() const {
  return client_->BrowserCompositorIOSGetGutterColor();
}

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

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

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

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

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

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

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

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

void BrowserCompositorIOS::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 BrowserCompositorIOS::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 BrowserCompositorIOS::ForceNewSurfaceForTesting() {
  float current_device_scale_factor =
      client_->GetCurrentScreenInfo().device_scale_factor;
  client_->SetCurrentDeviceScaleFactor(current_device_scale_factor * 2.0f);
  UpdateSurfaceFromUIView(dfh_size_dip_);
}

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

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

  return dfh_local_surface_id_allocator_.GetCurrentLocalSurfaceId();
}

void BrowserCompositorIOS::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 BrowserCompositorIOS::LayerDestroyed(ui::Layer* layer) {
  CHECK_EQ(layer, parent_ui_layer_, base::NotFatalUntil::M152);
  SetParentUiLayer(nullptr);
}

ui::Compositor* BrowserCompositorIOS::GetCompositor() const {
  if (parent_ui_layer_) {
    return parent_ui_layer_->GetCompositor();
  }
  return compositor_.get();
}

void BrowserCompositorIOS::InvalidateSurfaceAllocationGroup() {
  local_surface_id_allocator_.Invalidate(
      /*also_invalidate_allocation_group=*/true);
}

cc::DeadlinePolicy BrowserCompositorIOS::GetDeadlinePolicy(
    bool is_resize) const {
  // Determined empirically for smoothness. Don't wait for non-resize frames,
  // as it can cause jank at new tab creation.
  // https://crbug.com/855364
  uint32_t frames_to_wait = is_resize ? 8 : 0;

  // When using the RecyclableCompositor, never wait for frames to arrive
  // (surface sync is managed by the Suspend/Unsuspend lock).
  if (compositor_) {
    frames_to_wait = 0;
  }

  return cc::DeadlinePolicy::UseSpecifiedDeadline(frames_to_wait);
}

void BrowserCompositorIOS::UpdateSurface(
    const gfx::Size& size_pixels,
    float scale_factor,
    const gfx::DisplayColorSpaces& display_color_spaces) {
  if (size_pixels != size_pixels_ || scale_factor != scale_factor_) {
    size_pixels_ = size_pixels;
    scale_factor_ = scale_factor;
    local_surface_id_allocator_.GenerateId();
    viz::LocalSurfaceId local_surface_id =
        local_surface_id_allocator_.GetCurrentLocalSurfaceId();
    compositor_->SetScaleAndSize(scale_factor_, size_pixels_, local_surface_id);
  }
  if (display_color_spaces != display_color_spaces_) {
    display_color_spaces_ = display_color_spaces;
    compositor_->SetDisplayColorSpaces(display_color_spaces_);
  }
}

void BrowserCompositorIOS::InvalidateSurface() {
  size_pixels_ = gfx::Size();
  scale_factor_ = 1.f;
  local_surface_id_allocator_.Invalidate(
      /*also_invalidate_allocation_group=*/true);
}

void BrowserCompositorIOS::Suspend() {
  CHECK(compositor_, base::NotFatalUntil::M152);
  // Requests a compositor lock without a timeout.
  compositor_suspended_lock_ =
      compositor_->GetCompositorLock(nullptr, base::TimeDelta());
}

void BrowserCompositorIOS::Unsuspend() {
  compositor_suspended_lock_ = nullptr;
}

}  // namespace content
