// Copyright 2013 The Flutter 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 "flutter/shell/platform/embedder/embedder_external_view_embedder.h"

#include <algorithm>

#include "flutter/shell/platform/embedder/embedder_layers.h"
#include "flutter/shell/platform/embedder/embedder_render_target.h"
#include "third_party/skia/include/gpu/GrContext.h"

namespace flutter {

EmbedderExternalViewEmbedder::EmbedderExternalViewEmbedder(
    const CreateRenderTargetCallback& create_render_target_callback,
    const PresentCallback& present_callback)
    : create_render_target_callback_(create_render_target_callback),
      present_callback_(present_callback) {
  FML_DCHECK(create_render_target_callback_);
  FML_DCHECK(present_callback_);
}

EmbedderExternalViewEmbedder::~EmbedderExternalViewEmbedder() = default;

void EmbedderExternalViewEmbedder::SetSurfaceTransformationCallback(
    SurfaceTransformationCallback surface_transformation_callback) {
  surface_transformation_callback_ = surface_transformation_callback;
}

SkMatrix EmbedderExternalViewEmbedder::GetSurfaceTransformation() const {
  if (!surface_transformation_callback_) {
    return SkMatrix{};
  }

  return surface_transformation_callback_();
}

void EmbedderExternalViewEmbedder::Reset() {
  pending_views_.clear();
  composition_order_.clear();
}

// |ExternalViewEmbedder|
void EmbedderExternalViewEmbedder::CancelFrame() {
  Reset();
}

// |ExternalViewEmbedder|
void EmbedderExternalViewEmbedder::BeginFrame(
    SkISize frame_size,
    GrContext* context,
    double device_pixel_ratio,
    fml::RefPtr<fml::RasterThreadMerger> raster_thread_merger) {
  Reset();

  pending_frame_size_ = frame_size;
  pending_device_pixel_ratio_ = device_pixel_ratio;
  pending_surface_transformation_ = GetSurfaceTransformation();

  static const auto kRootViewIdentifier =
      EmbedderExternalView::ViewIdentifier{};

  pending_views_[kRootViewIdentifier] = std::make_unique<EmbedderExternalView>(
      pending_frame_size_, pending_surface_transformation_);
  composition_order_.push_back(kRootViewIdentifier);
}

// |ExternalViewEmbedder|
void EmbedderExternalViewEmbedder::PrerollCompositeEmbeddedView(
    int view_id,
    std::unique_ptr<EmbeddedViewParams> params) {
  FML_DCHECK(pending_views_.count(view_id) == 0);

  pending_views_[view_id] = std::make_unique<EmbedderExternalView>(
      pending_frame_size_,              // frame size
      pending_surface_transformation_,  // surface xformation
      view_id,                          // view identifier
      std::move(params)                 // embedded view params
  );
  composition_order_.push_back(view_id);
}

// |ExternalViewEmbedder|
SkCanvas* EmbedderExternalViewEmbedder::GetRootCanvas() {
  auto found = pending_views_.find(EmbedderExternalView::ViewIdentifier{});
  if (found == pending_views_.end()) {
    FML_DLOG(WARNING)
        << "No root canvas could be found. This is extremely unlikely and "
           "indicates that the external view embedder did not receive the "
           "notification to begin the frame.";
    return nullptr;
  }
  return found->second->GetCanvas();
}

// |ExternalViewEmbedder|
std::vector<SkCanvas*> EmbedderExternalViewEmbedder::GetCurrentCanvases() {
  std::vector<SkCanvas*> canvases;
  for (const auto& view : pending_views_) {
    const auto& external_view = view.second;
    // This method (for legacy reasons) expects non-root current canvases.
    if (!external_view->IsRootView()) {
      canvases.push_back(external_view->GetCanvas());
    }
  }
  return canvases;
}

// |ExternalViewEmbedder|
SkCanvas* EmbedderExternalViewEmbedder::CompositeEmbeddedView(int view_id) {
  auto found = pending_views_.find(view_id);
  if (found == pending_views_.end()) {
    FML_DCHECK(false) << "Attempted to composite a view that was not "
                         "pre-rolled.";
    return nullptr;
  }
  return found->second->GetCanvas();
}

static FlutterBackingStoreConfig MakeBackingStoreConfig(
    const SkISize& backing_store_size) {
  FlutterBackingStoreConfig config = {};

  config.struct_size = sizeof(config);

  config.size.width = backing_store_size.width();
  config.size.height = backing_store_size.height();

  return config;
}

// |ExternalViewEmbedder|
void EmbedderExternalViewEmbedder::SubmitFrame(
    GrContext* context,
    std::unique_ptr<SurfaceFrame> frame) {
  auto [matched_render_targets, pending_keys] =
      render_target_cache_.GetExistingTargetsInCache(pending_views_);

  // This is where unused render targets will be collected. Control may flow to
  // the embedder. Here, the embedder has the opportunity to trample on the
  // OpenGL context.
  //
  // For optimum performance, we should tell the render target cache to clear
  // its unused entries before allocating new ones. This collection step before
  // allocating new render targets ameliorates peak memory usage within the
  // frame. But, this causes an issue in a known internal embedder. To work
  // around this issue while that embedder migrates, collection of render
  // targets is deferred after the presentation.
  //
  // @warning: Embedder may trample on our OpenGL context here.
  auto deferred_cleanup_render_targets =
      render_target_cache_.ClearAllRenderTargetsInCache();

  for (const auto& pending_key : pending_keys) {
    const auto& external_view = pending_views_.at(pending_key);

    // If the external view does not have engine rendered contents, it makes no
    // sense to ask to embedder to create a render target for us as we don't
    // intend to render into it and ask the embedder for presentation anyway.
    // Save some memory.
    if (!external_view->HasEngineRenderedContents()) {
      continue;
    }

    // This is the size of render surface we want the embedder to create for
    // us. As or right now, this is going to always be equal to the frame size
    // post transformation. But, in case optimizations are applied that make
    // it so that embedder rendered into surfaces that aren't full screen,
    // this assumption will break. So it's just best to ask view for its size
    // directly.
    const auto render_surface_size = external_view->GetRenderSurfaceSize();

    const auto backing_store_config =
        MakeBackingStoreConfig(render_surface_size);

    // This is where the embedder will create render targets for us. Control
    // flow to the embedder makes the engine susceptible to having the embedder
    // trample on the OpenGL context. Before any Skia operations are performed,
    // the context must be reset.
    //
    // @warning: Embedder may trample on our OpenGL context here.
    auto render_target =
        create_render_target_callback_(context, backing_store_config);

    if (!render_target) {
      FML_LOG(ERROR) << "Embedder did not return a valid render target.";
      return;
    }
    matched_render_targets[pending_key] = std::move(render_target);
  }

  // The OpenGL context could have been trampled by the embedder at this point
  // as it attempted to collect old render targets and create new ones. Tell
  // Skia to not rely on existing bindings.
  if (context) {
    context->resetContext(kAll_GrBackendState);
  }

  // Scribble embedder provide render targets. The order in which we scribble
  // into the buffers is irrelevant to the presentation order.
  for (const auto& render_target : matched_render_targets) {
    if (!pending_views_.at(render_target.first)
             ->Render(*render_target.second)) {
      FML_LOG(ERROR)
          << "Could not render into the embedder supplied render target.";
      return;
    }
  }

  // We are going to be transferring control back over to the embedder there the
  // context may be trampled upon again. Flush all operations to the underlying
  // rendering API.
  //
  // @warning: Embedder may trample on our OpenGL context here.
  if (context) {
    context->flushAndSubmit();
  }

  // Submit the scribbled layer to the embedder for presentation.
  //
  // @warning: Embedder may trample on our OpenGL context here.
  {
    EmbedderLayers presented_layers(pending_frame_size_,
                                    pending_device_pixel_ratio_,
                                    pending_surface_transformation_);
    // In composition order, submit backing stores and platform views to the
    // embedder.
    for (const auto& view_id : composition_order_) {
      // If the external view has a platform view, ask the emebdder to place it
      // before the Flutter rendered contents for that interleaving level.
      const auto& external_view = pending_views_.at(view_id);
      if (external_view->HasPlatformView()) {
        presented_layers.PushPlatformViewLayer(
            external_view->GetViewIdentifier()
                .platform_view_id.value(),           // view id
            *external_view->GetEmbeddedViewParams()  // view params
        );
      }

      // If the view has engine rendered contents, ask the embedder to place
      // Flutter rendered contents for this interleaving level on top of a
      // platform view.
      if (external_view->HasEngineRenderedContents()) {
        const auto& exteral_render_target = matched_render_targets.at(view_id);
        presented_layers.PushBackingStoreLayer(
            exteral_render_target->GetBackingStore());
      }
    }

    // Flush the layer description down to the embedder for presentation.
    //
    // @warning: Embedder may trample on our OpenGL context here.
    presented_layers.InvokePresentCallback(present_callback_);
  }

  // See why this is necessary in the comment where this collection in realized.
  //
  // @warning: Embedder may trample on our OpenGL context here.
  deferred_cleanup_render_targets.clear();

  // Hold all rendered layers in the render target cache for one frame to
  // see if they may be reused next frame.
  for (auto& render_target : matched_render_targets) {
    render_target_cache_.CacheRenderTarget(render_target.first,
                                           std::move(render_target.second));
  }

  frame->Submit();
}

}  // namespace flutter
