// 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 "chrome/browser/ui/views/compose/chrome_compose_dialog_controller.h"

#include "base/functional/callback.h"
#include "base/task/sequenced_task_runner.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
#include "chrome/browser/ui/dialogs/browser_dialogs.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "components/compose/core/browser/compose_client.h"
#include "components/compose/core/browser/compose_features.h"
#include "components/compose/core/browser/compose_metrics.h"
#include "components/compose/core/browser/config.h"
#include "ui/views/bubble/bubble_border.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"

namespace chrome {

std::unique_ptr<compose::ComposeDialogController> ShowComposeDialog(
    content::WebContents& web_contents,
    const gfx::RectF& element_bounds_in_screen,
    compose::ComposeClient::FieldIdentifier field_ids) {
  // The Compose dialog is not anchored to any particular View. Pass the
  // BrowserView so that it still knows about the Browser window, which is
  // needed to access the correct ColorProvider for theming.
  views::View* anchor_view = BrowserView::GetBrowserViewForBrowser(
      GlobalBrowserCollection::GetInstance()->FindBrowserWithTab(
          &web_contents));
  auto controller =
      std::make_unique<ChromeComposeDialogController>(&web_contents, field_ids);
  controller->ShowComposeDialog(anchor_view, element_bounds_in_screen);
  return controller;
}

}  // namespace chrome

ChromeComposeDialogController::~ChromeComposeDialogController() = default;

// TOOD(b/301371110): This function should accept an argument indicating whether
// it was called from the context menu. If so, open by popping in. Otherwise,
// open with an expanding animation.
void ChromeComposeDialogController::ShowComposeDialog(
    views::View* anchor_view,
    const gfx::RectF& element_bounds_in_screen) {
  if (!web_contents_) {
    compose::LogOpenComposeDialogResult(
        compose::OpenComposeDialogResult::kNoWebContents);
    return;
  }

  Profile* profile =
      Profile::FromBrowserContext(web_contents_->GetBrowserContext());
  auto bubble_wrapper =
      std::make_unique<WebUIContentsWrapperT<ComposeUntrustedUI>>(
          GURL(chrome::kChromeUIUntrustedComposeUrl), profile,
          IDS_COMPOSE_DIALOG_TITLE);

  // This WebUI needs to know the calling BrowserContents so that the compose
  // request/result can be properly associated with the triggering form.
  bubble_wrapper->GetWebUIController()->set_triggering_web_contents(
      web_contents_.get());

  // The element will not be visible if it is outside the Browser View bounds,
  // so clamp the element bounds to be within them.
  gfx::Rect clamped_element_bounds =
      gfx::ToRoundedRect(element_bounds_in_screen);
  clamped_element_bounds.Intersect(anchor_view->GetBoundsInScreen());

  auto compose_dialog_view = std::make_unique<ComposeDialogView>(
      anchor_view, std::move(bubble_wrapper), clamped_element_bounds,
      views::BubbleBorder::Arrow::TOP_CENTER);
  bubble_ = compose_dialog_view->GetWeakPtr();
  views::BubbleDialogDelegateView::CreateBubble(std::move(compose_dialog_view));
  if (bubble_) {
    compose::LogOpenComposeDialogResult(
        compose::OpenComposeDialogResult::kSuccess);
    // This must be called after CreateBubble, as that resets the
    // |adjust_if_offscreen| field to the platform-dependent default.
    bubble_->set_adjust_if_offscreen(true);

    if (base::FeatureList::IsEnabled(
            compose::features::kEnableComposeSavedStateNotification)) {
      if (bubble_->GetWidget()) {
        widget_observation_.Observe(bubble_->GetWidget());
      }
    }
  } else {
    compose::LogOpenComposeDialogResult(
        compose::OpenComposeDialogResult::kFailedCreatingComposeDialogView);
  }
}

WebUIContentsWrapperT<ComposeUntrustedUI>*
ChromeComposeDialogController::GetBubbleWrapper() const {
  if (bubble_) {
    return bubble_->bubble_wrapper();
  }
  return nullptr;
}

void ChromeComposeDialogController::ShowUI(
    base::OnceClosure focus_lost_callback) {
  focus_lost_callback_ = std::move(focus_lost_callback);
  if (bubble_) {
    bubble_->ShowUI();
  }
}

// TODO(b/300939629): Flesh out implementation and cover other closing paths.
void ChromeComposeDialogController::Close() {
  // This will no-op if there is no observation.
  widget_observation_.Reset();
  focus_lost_callback_.Reset();
  auto* wrapper = GetBubbleWrapper();
  if (wrapper) {
    wrapper->CloseUI();
  }
}

bool ChromeComposeDialogController::IsDialogShowing() {
  return bubble_ && !bubble_->GetWidget()->IsClosed();
}

void ChromeComposeDialogController::OnWidgetDestroying(views::Widget* widget) {
  if (focus_lost_callback_) {
    const compose::Config& config = compose::GetComposeConfig();
    // TODO(b/328730979): Add slight delay so that focus lost callback can be
    // called after all focus-related events have been processed.
    base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(&ChromeComposeDialogController::OnAfterWidgetDestroyed,
                       weak_ptr_factory_.GetWeakPtr()),
        config.focus_lost_delay);
  }
  // This will no-op if there is no observation.
  widget_observation_.Reset();
}

const compose::ComposeClient::FieldIdentifier&
ChromeComposeDialogController::GetFieldIds() {
  return field_ids_;
}

void ChromeComposeDialogController::OnAfterWidgetDestroyed() {
  if (focus_lost_callback_) {
    std::move(focus_lost_callback_).Run();
  }
}

ChromeComposeDialogController::ChromeComposeDialogController(
    content::WebContents* web_contents,
    compose::ComposeClient::FieldIdentifier field_ids)
    : web_contents_(web_contents->GetWeakPtr()), field_ids_(field_ids) {}
