// Copyright 2026 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/surface_embed/renderer/surface_embed_web_plugin.h"

#include "base/notimplemented.h"
#include "cc/layers/picture_layer.h"
#include "cc/layers/surface_layer.h"
#include "cc/paint/paint_image_builder.h"
#include "cc/paint/paint_op.h"
#include "components/viz/common/surfaces/frame_sink_id.h"
#include "components/viz/common/surfaces/surface_id.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_frame_observer.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/common/frame/frame_visual_properties.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/public/web/web_document.h"
#include "third_party/blink/public/web/web_element.h"
#include "third_party/blink/public/web/web_frame_widget.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_plugin_container.h"
#include "third_party/blink/public/web/web_plugin_params.h"
#include "third_party/blink/public/web/web_view.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkBlendMode.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkImage.h"
#include "ui/accessibility/ax_node_id_forward.h"
#include "ui/gfx/geometry/size.h"

namespace surface_embed {

class SurfaceEmbedWebPlugin::AccessibilityObserver
    : public content::RenderFrameObserver {
 public:
  AccessibilityObserver(content::RenderFrame* render_frame,
                        SurfaceEmbedWebPlugin* plugin)
      : content::RenderFrameObserver(render_frame), plugin_(plugin) {}
  AccessibilityObserver(const AccessibilityObserver&) = delete;
  AccessibilityObserver& operator=(const AccessibilityObserver&) = delete;
  ~AccessibilityObserver() override = default;

  void AccessibilityModeChanged(const ui::AXMode& mode) override {
    if (mode.has_mode(ui::AXMode::kWebContents)) {
      plugin_->OnAccessibilityModeEnabled();
    }
  }

  void OnDestruct() override {}

 private:
  raw_ptr<SurfaceEmbedWebPlugin> plugin_;
};

namespace {

base::UnguessableToken DecodeContentId(const std::string& id_string) {
  if (id_string.empty()) {
    return base::UnguessableToken();
  }
  std::optional<base::UnguessableToken> maybe_id =
      base::UnguessableToken::DeserializeFromString(id_string);
  return maybe_id.has_value() ? *maybe_id : base::UnguessableToken();
}

}  // namespace

// static
SurfaceEmbedWebPlugin* SurfaceEmbedWebPlugin::Create(
    content::RenderFrame* render_frame,
    const blink::WebPluginParams& params) {
  // Read the content ID from the data-content-id attribute
  base::UnguessableToken contents_id;
  for (size_t i = 0; i < params.attribute_names.size(); ++i) {
    if (params.attribute_names[i].Utf8() == "data-content-id") {
      contents_id = DecodeContentId(params.attribute_values[i].Utf8());
      break;
    }
  }

  return new SurfaceEmbedWebPlugin(contents_id, render_frame, params);
}

SurfaceEmbedWebPlugin::SurfaceEmbedWebPlugin(
    const base::UnguessableToken& contents_id,
    content::RenderFrame* render_frame,
    const blink::WebPluginParams& params)
    : contents_id_(contents_id) {
  render_frame->GetRemoteAssociatedInterfaces()->GetInterface(&host_);
  accessibility_observer_ =
      std::make_unique<AccessibilityObserver>(render_frame, this);
}

SurfaceEmbedWebPlugin::~SurfaceEmbedWebPlugin() = default;

bool SurfaceEmbedWebPlugin::Initialize(blink::WebPluginContainer* container) {
  container_ = container;
  InitializeSurfaceLayer();

  CHECK(host_);
  mojo::PendingAssociatedRemote<mojom::SurfaceEmbed> pending_remote =
      receiver_.BindNewEndpointAndPassRemote();
  host_.set_disconnect_handler(base::BindOnce(
      &SurfaceEmbedWebPlugin::OnHostDisconnected, base::Unretained(this)));
  receiver_.set_disconnect_handler(base::BindOnce(
      &SurfaceEmbedWebPlugin::OnHostDisconnected, base::Unretained(this)));

  // Set up the SurfaceEmbed interface first.
  host_->SetSurfaceEmbed(std::move(pending_remote));

  // Then attach with the content ID.
  if (!contents_id_.is_empty()) {
    host_->AttachConnector(contents_id_);
  }

  // If accessibility was already enabled before the plugin was created,
  // send the info now. AccessibilityModeChanged only fires on transitions.
  SendAccessibilityInfo();

  return true;
}

void SurfaceEmbedWebPlugin::OnAccessibilityModeEnabled() {
  SendAccessibilityInfo();
}

void SurfaceEmbedWebPlugin::SendAccessibilityInfo() {
  if (!container_ || !host_) {
    return;
  }

  content::RenderFrame* render_frame =
      accessibility_observer_ ? accessibility_observer_->render_frame()
                              : nullptr;
  if (!render_frame || !render_frame->GetRenderAccessibility()) {
    return;
  }

  // The embed element's AX ID identifies the parent-side node that the child
  // accessibility tree is stitched onto.
  //
  // Note the plugin is always created before the AX tree is fully built, so we
  // can't fetch the AX ID at this time. However, AX ID always maps to DOM Node
  // ID so we can use this as a substitute. AXObjectCacheImpl keys every
  // DOM-backed AXObject by its DOMNodeId and derives the AXID from
  // DOMNodeIds::ExistingIdForNode(), so the id read here is exactly the AXID
  // the object will have once it is created.
  //
  // The alternative, blink::WebAXObject::FromWebNode(), cannot be used here: it
  // calls AXObjectCache::UpdateAXForAllDocuments(), which forces the *parent*
  // renderer to synchronously serialize its entire AX tree. That serialization
  // re-emits the persisted parent->child stitch link, so the parent->child link
  // reaches the browser ahead of the child->parent link being requested here.
  // A child->parent link arriving first is harmless (AX clients traverse from
  // parent to child), but a parent->child link with no reciprocal child->parent
  // link is a transient half-stitched state that crashes AX consumers.
  //
  // TODO(accessibility): Expose a supported way to obtain the AX ID for a node
  // without forcing a full AX tree update, and use it here instead of relying
  // on the DOM Node ID / AX ID equivalence.
  ui::AXNodeID ax_id = container_->GetElement().GetDomNodeId();
  if (ax_id == ui::kInvalidAXNodeID) {
    return;
  }

  host_->SetParentAccessibilityInfo(ax_id);
}

void SurfaceEmbedWebPlugin::Destroy() {
  paint_holding_helper_.ClearPaintHolding(layer_.get());
  if (container_) {
    container_->SetCcLayer(nullptr);
    container_ = nullptr;
  }
  layer_.reset();
  ReleaseCrashedLayer();

  receiver_.reset();
  host_.reset();

  delete this;
}

blink::WebPluginContainer* SurfaceEmbedWebPlugin::Container() const {
  return container_;
}

void SurfaceEmbedWebPlugin::UpdateAllLifecyclePhases(
    blink::DocumentUpdateReason reason) {}

void SurfaceEmbedWebPlugin::Paint(cc::PaintCanvas* canvas,
                                  const gfx::Rect& rect) {
  // No action needed as we're using a compositor layer to render.
}

viz::FrameSinkId SurfaceEmbedWebPlugin::GetFrameSinkId() {
  return frame_sink_id_;
}

void SurfaceEmbedWebPlugin::UpdateGeometry(const gfx::Rect& window_rect,
                                           const gfx::Rect& clip_rect,
                                           const gfx::Rect& unobscured_rect,
                                           bool is_visible) {
  // Note: Layer bounds are set by WebPluginContainerImpl::Paint()
  // so we don't need to set them here for the time being.

  if (last_window_rect_ == window_rect && last_clip_rect_ == clip_rect &&
      last_unobscured_rect_ == unobscured_rect &&
      last_is_visible_ == is_visible && !frame_sink_id_changed_) {
    return;
  }

  last_window_rect_ = window_rect;
  last_clip_rect_ = clip_rect;
  last_unobscured_rect_ = unobscured_rect;
  last_is_visible_ = is_visible;

  if (frame_sink_id_.is_valid()) {
    SynchronizeVisualProperties(/*allow_paint_holding=*/false);
  }
}

void SurfaceEmbedWebPlugin::UpdateFocus(bool focused,
                                        blink::mojom::FocusType focus_type) {
  if (host_) {
    host_->OnEmbedElementFocused(focused, focus_type);
  }
}

bool SurfaceEmbedWebPlugin::SupportsKeyboardFocus() const {
  return true;
}

void SurfaceEmbedWebPlugin::UpdateVisibility(bool visible) {
  bool prev_is_visible = last_is_visible_;
  last_is_visible_ = visible;

  if (last_is_visible_ != prev_is_visible && frame_sink_id_.is_valid()) {
    SynchronizeVisualProperties(/*allow_paint_holding=*/false);
  }
}

void SurfaceEmbedWebPlugin::UpdateRenderThrottlingStatus(bool is_throttled,
                                                         bool subtree_throttled,
                                                         bool display_locked) {
  if (last_is_throttled_ == is_throttled &&
      last_subtree_throttled_ == subtree_throttled &&
      last_display_locked_ == display_locked) {
    return;
  }

  last_is_throttled_ = is_throttled;
  last_subtree_throttled_ = subtree_throttled;
  last_display_locked_ = display_locked;

  if (host_) {
    host_->OnEmbedElementThrottlingStatusChanged(
        mojom::RenderThrottlingStatus::New(is_throttled, subtree_throttled,
                                           display_locked));
  }
}

blink::WebInputEventResult SurfaceEmbedWebPlugin::HandleInputEvent(
    const blink::WebCoalescedInputEvent& event,
    ui::Cursor* cursor) {
  return blink::WebInputEventResult::kNotHandled;
}

void SurfaceEmbedWebPlugin::DidReceiveResponse(
    const blink::WebURLResponse& response) {
  NOTIMPLEMENTED();
}

void SurfaceEmbedWebPlugin::DidReceiveData(base::span<const char> data) {
  NOTIMPLEMENTED();
}

void SurfaceEmbedWebPlugin::DidFinishLoading() {
  NOTIMPLEMENTED();
}

void SurfaceEmbedWebPlugin::DidFailLoading(const blink::WebURLError& error) {
  NOTIMPLEMENTED();
}

void SurfaceEmbedWebPlugin::InitializeSurfaceLayer() {
  parent_local_surface_id_allocator_ =
      std::make_unique<viz::ParentLocalSurfaceIdAllocator>();
  // We'll be embedding an outside surface layer.
  layer_ = cc::SurfaceLayer::Create();
  layer_->SetIsDrawable(true);
  layer_->SetContentsOpaque(true);
  layer_->SetBackgroundColor(SkColors::kTransparent);
  layer_->SetSurfaceHitTestable(true);
  // Don't use the layer for `container_` yet because it does not have a valid
  // surface id.
}

void SurfaceEmbedWebPlugin::SynchronizeVisualProperties(
    bool allow_paint_holding) {
  // Note: This is largely based on RemoteFrame's SynchronizeVisualProperties().
  // TODO(surface-embed): The following properties or pieces of functionality
  // have not yet been vetted as needed or correct implementation:
  // - css zoom between ancestor widget and embedding element (inclusive).
  // - viewport segments, do these need any adjustment for plugin location/size?
  // - compositor viewport, does it need to be more accurate (See RemoteFrame)?
  //   Right now it's the part of the plugin that's visible.
  // - cursor_accessibility_scale_factor
  // - propagate parameter (see RemoteFrame's implementation, do we need to do
  //   anything to propagate these changes through the embedded WebContents?)

  if (!frame_sink_id_.is_valid()) {
    return;
  }

  blink::FrameVisualProperties pending_visual_properties;

  // No support for auto-resize.
  pending_visual_properties.auto_resize_enabled = false;
  pending_visual_properties.min_size_for_auto_resize = gfx::Size();
  pending_visual_properties.max_size_for_auto_resize = gfx::Size();

  blink::WebFrameWidget* ancestor_widget =
      container_->GetDocument().GetFrame()->LocalRoot()->FrameWidget();
  CHECK(ancestor_widget);

  pending_visual_properties.zoom_level = ancestor_widget->GetZoomLevel();
  pending_visual_properties.css_zoom_factor =
      ancestor_widget->GetCSSZoomFactor();
  pending_visual_properties.page_scale_factor = container_->PageScaleFactor();
  pending_visual_properties.is_pinch_gesture_active =
      ancestor_widget->PinchGestureActiveInMainFrame();
  pending_visual_properties.screen_infos = ancestor_widget->GetScreenInfos();

  // For separate WebContents acting like an iframe, the "visible viewport" is
  // the portion of the plugin that is visible within the plugin bounds.
  pending_visual_properties.visible_viewport_size =
      gfx::Size(last_clip_rect_.width(), last_clip_rect_.height());

  const std::vector<gfx::Rect>& viewport_segments =
      ancestor_widget->ViewportSegments();
  pending_visual_properties.root_widget_viewport_segments.assign(
      viewport_segments.begin(), viewport_segments.end());
  pending_visual_properties.rect_in_local_root = last_window_rect_;
  pending_visual_properties.local_frame_size =
      gfx::Size(last_window_rect_.width(), last_window_rect_.height());
  pending_visual_properties.compositor_viewport = last_clip_rect_;
  pending_visual_properties.compositing_scale_factor = 1.0f;
  pending_visual_properties.cursor_accessibility_scale_factor = 1.0f;

  bool synchronized_props_changed =
      frame_sink_id_changed_ || !sent_visual_properties_ ||
      sent_visual_properties_->auto_resize_enabled !=
          pending_visual_properties.auto_resize_enabled ||
      sent_visual_properties_->min_size_for_auto_resize !=
          pending_visual_properties.min_size_for_auto_resize ||
      sent_visual_properties_->max_size_for_auto_resize !=
          pending_visual_properties.max_size_for_auto_resize ||
      sent_visual_properties_->local_frame_size !=
          pending_visual_properties.local_frame_size ||
      sent_visual_properties_->rect_in_local_root !=
          pending_visual_properties.rect_in_local_root ||
      sent_visual_properties_->screen_infos !=
          pending_visual_properties.screen_infos ||
      sent_visual_properties_->zoom_level !=
          pending_visual_properties.zoom_level ||
      sent_visual_properties_->css_zoom_factor !=
          pending_visual_properties.css_zoom_factor ||
      sent_visual_properties_->page_scale_factor !=
          pending_visual_properties.page_scale_factor ||
      sent_visual_properties_->compositing_scale_factor !=
          pending_visual_properties.compositing_scale_factor ||
      sent_visual_properties_->cursor_accessibility_scale_factor !=
          pending_visual_properties.cursor_accessibility_scale_factor ||
      sent_visual_properties_->is_pinch_gesture_active !=
          pending_visual_properties.is_pinch_gesture_active ||
      sent_visual_properties_->visible_viewport_size !=
          pending_visual_properties.visible_viewport_size ||
      sent_visual_properties_->compositor_viewport !=
          pending_visual_properties.compositor_viewport ||
      sent_visual_properties_->root_widget_viewport_segments !=
          pending_visual_properties.root_widget_viewport_segments ||
      !sent_last_is_visible_ || sent_last_is_visible_ != last_is_visible_;

  if (synchronized_props_changed) {
    parent_local_surface_id_allocator_->GenerateId();
  }

  auto local_surface_id =
      parent_local_surface_id_allocator_->GetCurrentLocalSurfaceId();
  pending_visual_properties.local_surface_id = local_surface_id;

  viz::SurfaceId surface_id(frame_sink_id_,
                            pending_visual_properties.local_surface_id);
  CHECK(surface_id.is_valid());
  paint_holding_helper_.SetSurfaceId(layer_.get(), surface_id,
                                     allow_paint_holding);
  if (container_) {
    container_->SetCcLayer(layer_.get());
    container_->ScheduleAnimation();
  }

  if (synchronized_props_changed) {
    host_->SynchronizeVisualProperties(pending_visual_properties,
                                       last_is_visible_);
    sent_visual_properties_ = pending_visual_properties;
    sent_last_is_visible_ = last_is_visible_;
    container_->ScheduleAnimation();
  }

  frame_sink_id_changed_ = false;
}

void SurfaceEmbedWebPlugin::OnHostDisconnected() {
  // We handle closing the connection unexpectedly via the sad plugin path,
  // since that provides fallback painting behavior suggesting that something
  // went wrong.
  ChildProcessGone();
}

void SurfaceEmbedWebPlugin::SetFrameSinkId(
    const ::viz::FrameSinkId& frame_sink_id,
    bool allow_paint_holding) {
  CHECK(container_);
  CHECK(frame_sink_id.is_valid());
  // Make sure we use a normal layer, not crash one.
  container_->SetCcLayer(layer_.get());
  ReleaseCrashedLayer();

  // The same ParentLocalSurfaceIdAllocator cannot provide LocalSurfaceIds for
  // two different frame sinks, so recreate it here.
  if (frame_sink_id_ != frame_sink_id) {
    parent_local_surface_id_allocator_ =
        std::make_unique<viz::ParentLocalSurfaceIdAllocator>();
  }
  frame_sink_id_ = frame_sink_id;
  frame_sink_id_changed_ = true;

  SynchronizeVisualProperties(allow_paint_holding);
}

void SurfaceEmbedWebPlugin::UpdateLocalSurfaceIdFromChild(
    const ::viz::LocalSurfaceId& local_surface_id) {
  if (!parent_local_surface_id_allocator_->UpdateFromChild(local_surface_id)) {
    return;
  }

  // The viz::LocalSurfaceId has changed so we call SynchronizeVisualProperties
  // here to embed it.
  SynchronizeVisualProperties(/*allow_paint_holding=*/false);
}

void SurfaceEmbedWebPlugin::ChildProcessGone() {
  paint_holding_helper_.ClearPaintHolding(layer_.get());
  frame_sink_id_ = viz::FrameSinkId();
  crashed_layer_ = cc::PictureLayer::Create(this);
  crashed_layer_->SetMasksToBounds(true);
  crashed_layer_->SetIsDrawable(true);
  container_->SetCcLayer(crashed_layer_.get());
  container_->ScheduleAnimation();
}

void SurfaceEmbedWebPlugin::RequestFocusOnEmbedElement(
    RequestFocusOnEmbedElementCallback callback) {
  if (container_) {
    container_->GetElement().Focus();
  }
  std::move(callback).Run();
}

void SurfaceEmbedWebPlugin::AdvanceFocusFromEmbedElement(bool reverse) {
  if (!container_) {
    return;
  }
  if (container_->GetDocument().FocusedElement() != container_->GetElement()) {
    return;
  }
  if (auto* frame = container_->GetDocument().GetFrame()) {
    if (auto* view = frame->View()) {
      view->AdvanceFocus(reverse);
    }
  }
}

scoped_refptr<cc::DisplayItemList>
SurfaceEmbedWebPlugin::PaintContentsToDisplayList() {
  blink::WebFrameWidget* ancestor_widget =
      container_->GetDocument().GetFrame()->LocalRoot()->FrameWidget();
  auto device_scale_factor =
      ancestor_widget->GetOriginalScreenInfo().device_scale_factor;
  // Adapted from ChildFrameCompositingHelper::PaintContentsToDisplayList().
  auto layer_size = crashed_layer_->bounds();
  auto display_list = base::MakeRefCounted<cc::DisplayItemList>();
  display_list->StartPaint();
  display_list->push<cc::DrawColorOp>(SkColors::kGray, SkBlendMode::kSrc);

  SkBitmap* sad_bitmap = blink::Platform::Current()->GetSadPageBitmap();
  if (sad_bitmap) {
    float paint_width = sad_bitmap->width() * device_scale_factor;
    float paint_height = sad_bitmap->height() * device_scale_factor;
    if (layer_size.width() >= paint_width &&
        layer_size.height() >= paint_height) {
      float x = (layer_size.width() - paint_width) / 2.0f;
      float y = (layer_size.height() - paint_height) / 2.0f;
      if (device_scale_factor != 1.f) {
        display_list->push<cc::SaveOp>();
        display_list->push<cc::TranslateOp>(x, y);
        display_list->push<cc::ScaleOp>(device_scale_factor,
                                        device_scale_factor);
        x = 0;
        y = 0;
      }

      auto image = cc::PaintImageBuilder::WithDefault()
                       .set_id(cc::PaintImage::GetNextId())
                       .set_image(SkImages::RasterFromBitmap(*sad_bitmap),
                                  cc::PaintImage::GetNextContentId())
                       .TakePaintImage();
      display_list->push<cc::DrawImageOp>(image, x, y);

      if (device_scale_factor != 1.f) {
        display_list->push<cc::RestoreOp>();
      }
    }
  }
  display_list->EndPaintOfUnpaired(gfx::Rect(layer_size));
  display_list->Finalize();
  return display_list;
}

bool SurfaceEmbedWebPlugin::FillsBoundsCompletely() const {
  return true;
}

void SurfaceEmbedWebPlugin::ReleaseCrashedLayer() {
  if (crashed_layer_) {
    crashed_layer_->ClearClient();
    crashed_layer_.reset();
  }
}

}  // namespace surface_embed
