// 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 "fuchsia_web/shell/present_frame.h"

#include <fuchsia/ui/policy/cpp/fidl.h>
#include <fuchsia/ui/scenic/cpp/fidl.h>
#include <lib/sys/cpp/component_context.h>
#include <lib/sys/cpp/service_directory.h>
#include <lib/ui/scenic/cpp/view_creation_tokens.h>
#include <lib/ui/scenic/cpp/view_ref_pair.h>
#include <lib/ui/scenic/cpp/view_token_pair.h>
#include <stdint.h>
#include <zircon/rights.h>

#include <memory>
#include <utility>
#include <vector>

#include "base/check.h"
#include "base/fuchsia/fuchsia_logging.h"
#include "base/fuchsia/process_context.h"
#include "base/logging.h"

fuchsia::ui::views::ViewRef CloneViewRef(
    const fuchsia::ui::views::ViewRef& view_ref) {
  fuchsia::ui::views::ViewRef dup;
  zx_status_t status =
      view_ref.reference.duplicate(ZX_RIGHT_SAME_RIGHTS, &dup.reference);
  ZX_CHECK(status == ZX_OK, status) << "zx_object_duplicate";
  return dup;
}

absl::optional<fuchsia::element::GraphicalPresenterPtr> PresentFrame(
    fuchsia::web::Frame* frame,
    fidl::InterfaceHandle<fuchsia::element::AnnotationController>
        annotation_controller) {
  // TODO(http://crbug.com/1418433): Remove Scenic connection when Flatland
  // lands.
  fuchsia::ui::scenic::ScenicSyncPtr scenic_svc;
  zx_status_t status = base::ComponentContextForProcess()->svc()->Connect(
      scenic_svc.NewRequest());
  ZX_CHECK(status == ZX_OK, status) << "Couldn't connect to Scenic.";
  bool uses_flatland = false;
  status = scenic_svc->UsesFlatland(&uses_flatland);
  ZX_CHECK(status == ZX_OK, status) << "UsesFlatland()";

  if (uses_flatland) {
    // fuchsia.element.GraphicalPresenter is the only view presentation protocol
    // that supports Flatland, so we expect it to be provided.
    //
    // Note also that using a sync connection results in a 3-way deadlock
    // between web_engine, (Flatland) scene_manager, and scenic, so we use an
    // async connection for the Flatland branch.
    fuchsia::element::GraphicalPresenterPtr presenter =
        base::ComponentContextForProcess()
            ->svc()
            ->Connect<fuchsia::element::GraphicalPresenter>();
    presenter.set_error_handler([](zx_status_t status) {
      ZX_LOG(ERROR, status) << "GraphicalPresenter disconnected: ";
    });

    // Generate Flatland tokens and frame->CreateView2().
    scenic::ViewCreationTokenPair token_pair =
        scenic::ViewCreationTokenPair::New();
    fuchsia::element::ViewSpec view_spec;
    view_spec.set_viewport_creation_token(std::move(token_pair.viewport_token));
    view_spec.set_annotations({});

    fuchsia::element::ViewControllerPtr view_controller;
    presenter->PresentView(
        std::move(view_spec), std::move(annotation_controller),
        view_controller.NewRequest(),
        [](fuchsia::element::GraphicalPresenter_PresentView_Result result) {
          if (result.is_err()) {
            LOG(ERROR) << "PresentView failed to display the view, reason: "
                       << static_cast<uint32_t>(result.err());
          }
        });

    // Present a fullscreen view of |frame|.
    fuchsia::web::CreateView2Args create_view_args;
    create_view_args.set_view_creation_token(std::move(token_pair.view_token));
    frame->CreateView2(std::move(create_view_args));

    return presenter;
  }

  // TODO(http://crbug.com/1418433): Remove GFX-specific logic below this
  // comment when Flatland lands.

  // Sync connect to fuchsia.element.GraphicalPresenter so that we can fallback
  // to fuchsia.ui.policy.Presenter if unavailable.
  // TODO(http://crbug.com/1402457): Replace with async when removing fallback.
  fuchsia::element::GraphicalPresenterSyncPtr presenter;
  status = base::ComponentContextForProcess()->svc()->Connect(
      presenter.NewRequest());
  ZX_CHECK(status == ZX_OK, status)
      << "Couldn't connect to GraphicalPresenter.";

  // Generate GFX tokens and frame->CreateViewWithViewRef().
  auto view_tokens = scenic::ViewTokenPair::New();
  auto view_ref_pair = scenic::ViewRefPair::New();

  fuchsia::element::ViewSpec view_spec;
  view_spec.set_view_holder_token(std::move(view_tokens.view_holder_token));
  view_spec.set_view_ref(CloneViewRef(view_ref_pair.view_ref));
  view_spec.set_annotations({});

  fuchsia::element::ViewControllerSyncPtr view_controller;
  fuchsia::element::GraphicalPresenter_PresentView_Result present_view_result;
  status = presenter->PresentView(
      std::move(view_spec), std::move(annotation_controller),
      view_controller.NewRequest(), &present_view_result);

  // Note: We do not consider `present_view_result.is_err()` in the fallback
  // condition in case the FIDL call succeeds but the method reports an error.
  // This is because the only error type reported by the PresentView method is
  // INVALID_ARGS, which we have carefully avoided by:
  // * Providing a view_spec.view_holder_token and view_spec.view_ref (GFX)
  // * Not providing _both_ GFX Views and Flatland Views at once.
  //
  // Therefore, we expect that if the FIDL call succeeds, the presentation
  // should also succeed.
  if (status == ZX_OK) {
    DCHECK(!present_view_result.is_err())
        << "PresentView failed to display the view, reason: "
        << static_cast<uint32_t>(present_view_result.err());
  } else {
    // Fallback to connect to Root Presenter.
    // TODO(http://crbug.com/1402457): Remove fallback.
    LOG(INFO) << "PresentView failed to connect, reason: " << status
              << ". Falling back to fuchsia.ui.policy.Presenter.";
    auto root_presenter = base::ComponentContextForProcess()
                              ->svc()
                              ->Connect<fuchsia::ui::policy::Presenter>();

    // Replace the original ViewToken and ViewRefPair with new ones.
    view_tokens = scenic::ViewTokenPair::New();
    view_ref_pair = scenic::ViewRefPair::New();

    root_presenter->PresentOrReplaceView2(
        std::move(view_tokens.view_holder_token),
        CloneViewRef(view_ref_pair.view_ref), nullptr);
  }

  // Present a fullscreen view of |frame|.
  frame->CreateViewWithViewRef(std::move(view_tokens.view_token),
                               std::move(view_ref_pair.control_ref),
                               std::move(view_ref_pair.view_ref));

  return absl::nullopt;
}