// Copyright 2025 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/frame/layout/browser_view_layout_impl.h"

#include "base/functional/bind.h"
#include "build/build_config.h"
#include "chrome/browser/ui/immersive/immersive_mode_controller.h"
#include "chrome/browser/ui/views/bookmarks/bookmark_bar_view.h"
#include "chrome/browser/ui/views/frame/custom_corners_background.h"
#include "chrome/browser/ui/views/frame/layout/browser_view_layout_delegate.h"
#include "chrome/browser/ui/views/frame/multi_contents_view.h"
#include "ui/base/ui_base_features.h"
#include "ui/views/view.h"

#if BUILDFLAG(IS_MAC)
#include "chrome/browser/ui/views/frame/glass_frame_service.h"
#endif

// Proposed layout implementation.

BrowserViewLayoutImpl::ProposedLayout::ProposedLayout(
    const gfx::Rect& bounds_,
    std::optional<bool> visibility_)
    : bounds(bounds_), visibility(visibility_) {}
BrowserViewLayoutImpl::ProposedLayout::ProposedLayout() = default;
BrowserViewLayoutImpl::ProposedLayout::ProposedLayout(
    ProposedLayout&&) noexcept = default;
BrowserViewLayoutImpl::ProposedLayout&
BrowserViewLayoutImpl::ProposedLayout::operator=(ProposedLayout&&) noexcept =
    default;
BrowserViewLayoutImpl::ProposedLayout::~ProposedLayout() = default;

BrowserViewLayoutImpl::ProposedLayout&
BrowserViewLayoutImpl::ProposedLayout::AddChild(
    views::View* child,
    const gfx::Rect& bounds_,
    std::optional<bool> visibility_) {
  const auto emplace_result =
      children.emplace(child, ProposedLayout(bounds_, visibility_));
  CHECK(emplace_result.second)
      << "Already added layout for " << child->GetClassName();
  return emplace_result.first->second;
}

void BrowserViewLayoutImpl::ProposedLayout::HideViewIfNotPresent(
    views::View* child) {
  // See if the child view already exists in the layout.
  if (GetLayoutFor(child)) {
    return;
  }

  // If not, give it empty bounds and set it to invisible.
  AddChild(child, gfx::Rect(), false);
}

const BrowserViewLayoutImpl::ProposedLayout*
BrowserViewLayoutImpl::ProposedLayout::GetLayoutFor(
    const views::View* descendant) const {
  for (const auto& child : children) {
    if (child.first == descendant) {
      return &child.second;
    }
    if (auto* const result = child.second.GetLayoutFor(descendant)) {
      return result;
    }
  }
  return nullptr;
}

std::optional<gfx::Rect> BrowserViewLayoutImpl::ProposedLayout::GetBoundsFor(
    const views::View* descendant,
    const views::View* relative_to) const {
  const ProposedLayout* layout = GetLayoutFor(descendant);
  if (!layout) {
    return std::nullopt;
  }
  // Since layout bounds are relative to the parent, do the conversion from
  // there.
  return views::View::ConvertRectToTarget(descendant->parent(), relative_to,
                                          layout->bounds);
}

void BrowserViewLayoutImpl::ProposedLayout::ApplyLayout(
    views::View* root,
    SetViewVisibility set_view_visibility) && {
  for (auto& child : root->children()) {
    if (const auto it = children.find(child); it != children.end()) {
      // Need to tail-recurse here because otherwise, when we set the bounds of
      // the immediate child, this will automatically trigger a layout on all
      // of its children, which have not been properly arranged yet. This
      // results in a potential double-layout, or in extreme cases, bugs like
      // https://crbug.com/464220949
      std::move(it->second).ApplyLayout(child, set_view_visibility);
      child->SetBoundsRect(it->second.bounds);
      if (it->second.visibility) {
        set_view_visibility(child, *it->second.visibility);
      }
      children.erase(it);
    }
  }
  if (!children.empty()) {
    const views::View* const leftover = children.begin()->first;
    DUMP_WILL_BE_NOTREACHED()
        << "Unapplied layout remains for " << leftover->GetClassName() << " in "
        << root->GetClassName();
  }
}

std::string BrowserViewLayoutImpl::ProposedLayout::ToString(int depth) const {
  std::ostringstream oss;
  for (auto& [child, layout] : children) {
    oss << std::string(2 * depth, ' ') << child->GetClassName() << " at "
        << layout.bounds.ToString();
    if (layout.visibility) {
      oss << (*layout.visibility ? " VISIBLE" : " NOT VISIBLE");
    }
    oss << std::endl;
    oss << layout.ToString(depth + 1);
  }
  return oss.str();
}

// Common layout.

BrowserViewLayoutImpl::BrowserViewLayoutImpl(
    std::unique_ptr<BrowserViewLayoutDelegate> delegate,
    BrowserViewLayoutViews views)
    : BrowserViewLayout(std::move(delegate), std::move(views)) {
  glass_mode_subscription_ = this->delegate().AddOnGlassModeChangedCallback(
      base::BindRepeating(&BrowserViewLayoutImpl::OnGlassModeChangedCallback,
                          base::Unretained(this)),
      /*current_state_out=*/&in_glass_mode_);
}

BrowserViewLayoutImpl::~BrowserViewLayoutImpl() = default;

// Static helpers.

// static
bool BrowserViewLayoutImpl::IsParentedTo(const views::View* child,
                                         const views::View* parent) {
  return child && parent && child->parent() == parent;
}

// static
bool BrowserViewLayoutImpl::IsParentedToAndVisible(const views::View* child,
                                                   const views::View* parent) {
  return IsParentedTo(child, parent) && child->GetVisible();
}

// static
gfx::Rect BrowserViewLayoutImpl::GetBoundsWithExclusion(
    const BrowserLayoutParams& params,
    const views::View* view,
    int leading_margin,
    int trailing_margin) {
  const auto leading =
      leading_margin ? params.leading_exclusion.ContentWithPaddingAndInsets(
                           leading_margin, 0.f)
                     : params.leading_exclusion.ContentWithPadding();
  const auto trailing =
      trailing_margin ? params.trailing_exclusion.ContentWithPaddingAndInsets(
                            trailing_margin, 0.f)
                      : params.trailing_exclusion.ContentWithPadding();
  int height = base::ClampCeil(std::max(leading.height(), trailing.height()));
  if (height) {
    height = std::max(height, view->GetMinimumSize().height());
  } else {
    height = view->GetPreferredSize().height();
  }
  return gfx::Rect(
      /*x=*/params.visual_client_area.x() + leading.width(),
      /*y=*/params.visual_client_area.y(),
      /*width=*/params.visual_client_area.width() -
          (leading.width() + trailing.width()),
      /*height=*/height);
}

gfx::Rect BrowserViewLayoutImpl::GetTopContainerBoundsInParent(
    const gfx::Rect& local_bounds,
    const BrowserLayoutParams& parent_params) const {
  // Calculate the dimensions of the container.
  int top = local_bounds.y();
  const int height = local_bounds.height();
  if (height <= 0) {
    // Use an empty bounds.
    return gfx::Rect(parent_params.visual_client_area.origin(),
                     gfx::Size(local_bounds.width(), 0));
  }

  // In certain circumstances, the top container bounds require adjustment.
  if (delegate().IsTopControlsSlideBehaviorEnabled()) {
    // In slide mode, if the top container is hidden completely, it is placed
    // outside the window bounds.
    top =
        delegate().GetTopControlsSlideBehaviorShownRatio() == 0.0 ? -height : 0;
  } else if (auto* const controller = delegate().GetImmersiveModeController();
             controller && controller->IsEnabled()) {
    // If the immersive mode controller is animating the top container overlay,
    // it may be partly offscreen. The controller knows where the container
    // needs to be.
    top = controller->GetTopContainerVerticalOffset(
        gfx::Size(parent_params.visual_client_area.width(), height));
  }

  gfx::Rect bounds = local_bounds;
  bounds.set_y(top);
  bounds.Offset(parent_params.visual_client_area.OffsetFromOrigin());
  return bounds;
}

// Layout logic.

void BrowserViewLayoutImpl::Layout(views::View* host) {
  if (reentrancy_guard_) {
    return;
  }
  base::AutoReset<bool> guard_reset(&reentrancy_guard_, true);

  auto params = delegate().GetBrowserLayoutParams(/*use_browser_bounds=*/true);
  if (params.IsEmpty()) {
    return;
  }

  DoPreLayoutComputations(params);

  // If the top container is separate from the browser view, lay it out now.
  if (views().top_container &&
      views().top_container->parent() != views().browser_view) {
    // In slide/immersive mode, animating the top container is handled by
    // someone else, but there are adjustments that are needed to be made.
    ProposedLayout top_container_layout;

    // The computation for the top container components does not change.
    const gfx::Rect top_container_local_bounds = CalculateTopContainerLayout(
        top_container_layout, params, /*needs_exclusion=*/true);

    // Apply the child layouts for the top container.
    std::move(top_container_layout)
        .ApplyLayout(views().top_container,
                     [this](views::View* view, bool visible) {
                       SetViewVisibility(view, visible);
                     });

    // Position the top container in its parent, whatever that is.
    views().top_container->SetBoundsRect(
        GetTopContainerBoundsInParent(top_container_local_bounds, params));

    // In (for example) fullscreen-with-toolbar, if the size of the top
    // container changes, then the overall layout dimensions may also change.
    // See https://crbug.com/519626620 for more information.
    const auto new_params =
        delegate().GetBrowserLayoutParams(/*use_browser_bounds=*/true);
    if (params != new_params) {
      OnLayoutParamsChanged(params, new_params);
      params = new_params;
    }
  }

  // Lay out the browser view itself.
  auto layout = CalculateProposedLayout(params);
  dialog_top_ = GetDialogTop(layout);
  dialog_bottom_ = GetDialogBottom(layout);
  std::move(layout).ApplyLayout(host, [this](views::View* view, bool visible) {
    SetViewVisibility(view, visible);
  });

  // The normal clipping created by `View::Paint()` may not cover the bottom of
  // the TopContainerView at certain scale factor because both of the position
  // and the height might be rounded down. This function sets the clip path that
  // enlarges the height at 2 DPs to compensate this error (both origin and
  // size) that the canvas can cover the entire TopContainerView.  See
  // crbug.com/390669712 for more details.
  //
  // TODO(crbug.com/41344902): Remove this hack once the pixel canvas is enabled
  // on all aura platforms.  Note that macOS supports integer scale only, so
  // this isn't necessary on macOS.
  if (!features::IsPixelCanvasRecordingEnabled()) {
    const auto apply_bottom_paint_allowance = [](views::View* view) {
      constexpr int kBottomPaintAllowance = 2;
      view->SetClipPath(SkPath::Rect(SkRect::MakeWH(
          view->width(), view->height() + kBottomPaintAllowance)));
    };

    // Here are the views which require adjustment (add/remove as necessary).
    if (views().toolbar && views().toolbar->GetVisible()) {
      apply_bottom_paint_allowance(views().toolbar);
    }
    if (views().bookmark_bar && views().bookmark_bar->GetVisible()) {
      apply_bottom_paint_allowance(views().bookmark_bar);
    }
    apply_bottom_paint_allowance(views().top_container);
  }

  // Change how the top container is painted based on layout.
  auto* const background =
      views().top_container->background()->AsA<CustomCornersBackground>();
  CHECK(background)
      << "Expected top container to have a CustomCornersBackground.";
  ConfigureTopContainerBackground(params, background);

  // Do any additional adjustments required by the specific layout.
  DoPostLayoutVisualAdjustments(params);

  // Update bubbles (like the find bar).
  UpdateBubbles();

  DoPostLayoutCleanup();
}

void BrowserViewLayoutImpl::ConfigureTopContainerBackground(
    const BrowserLayoutParams& params,
    CustomCornersBackground* background) {
  if (is_fullscreen(delegate().GetBrowserWindowState())) {
    // When in immersive mode, top container is painted with the frame color.
    // The color matches the active frame, allowing the tabstrip to paint
    // correctly.
    background->SetVisible(true);
    background->SetPrimaryColor(ui::kColorFrameActive);
    background->SetCorners(CustomCornersBackground::Corners());
  } else {
    // No need to paint otherwise.
    background->SetVisible(false);
  }
}

void BrowserViewLayoutImpl::DoPreLayoutComputations(
    const BrowserLayoutParams& params) {}

void BrowserViewLayoutImpl::DoPostLayoutVisualAdjustments(
    const BrowserLayoutParams& params) {}

void BrowserViewLayoutImpl::DoPostLayoutCleanup() {}

void BrowserViewLayoutImpl::OnLayoutParamsChanged(
    const BrowserLayoutParams& old_params,
    const BrowserLayoutParams& new_params) {}

void BrowserViewLayoutImpl::OnGlassModeChangedCallback(bool in_glass_mode) {
  if (in_glass_mode == in_glass_mode_) {
    return;
  }
  in_glass_mode_ = in_glass_mode;
  OnGlassModeChanged();
}

void BrowserViewLayoutImpl::OnGlassModeChanged() {}

// Dialog positioning.

int BrowserViewLayoutImpl::GetDialogTop(const ProposedLayout& layout) const {
  const int kConstrainedWindowOverlap = 3;
  const auto* const browser_view = views().browser_view.get();
  if (const auto toolbar_rect =
          layout.GetBoundsFor(views().toolbar, browser_view)) {
    return toolbar_rect->bottom() - kConstrainedWindowOverlap;
  }
  return kConstrainedWindowOverlap;
}

int BrowserViewLayoutImpl::GetDialogBottom(const ProposedLayout& layout) const {
  const auto* const browser_view = views().browser_view.get();
  if (const auto contents_rect =
          layout.GetBoundsFor(views().multi_contents_view, browser_view)) {
    return contents_rect->bottom();
  }
  return browser_view->height();
}

gfx::Point BrowserViewLayoutImpl::GetDialogPosition(
    const gfx::Size& dialog_size) const {
  const auto params =
      delegate().GetBrowserLayoutParams(/*use_browser_bounds=*/false);
  if (params.IsEmpty()) {
    return gfx::Point();
  }

  // Calculate the dialog bounds in browser view space.
  const int browser_width = params.visual_client_area.width();
  const int dialog_x =
      params.visual_client_area.x() + (browser_width - dialog_size.width()) / 2;
  gfx::Rect dialog_rect(dialog_x, dialog_top_, dialog_size.width(),
                        dialog_size.height());

  // TODO: consider whether this should change in RTL?
  return gfx::Point(dialog_rect.origin());
}

gfx::Size BrowserViewLayoutImpl::GetMaximumDialogSize() const {
  const auto params =
      delegate().GetBrowserLayoutParams(/*use_browser_bounds=*/false);
  if (params.IsEmpty()) {
    return gfx::Size();
  }

  // This computation is irrespective of coordinate system (all coordinates
  // happen to be in browser view space).
  return gfx::Size(params.visual_client_area.width(),
                   dialog_bottom_ - dialog_top_);
}
