// Copyright 2012 The Chromium 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 "chrome/browser/ui/views/frame/browser_non_client_frame_view_ash.h"

#include <algorithm>

#include "ash/ash_layout_constants.h"
#include "ash/frame/caption_buttons/frame_caption_button_container_view.h"
#include "ash/frame/default_header_painter.h"
#include "ash/frame/frame_border_hit_test_controller.h"
#include "ash/frame/header_painter_util.h"
#include "ash/shell.h"
#include "base/profiler/scoped_tracker.h"
#include "build/build_config.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/profiles/profiles_state.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/views/frame/browser_frame.h"
#include "chrome/browser/ui/views/frame/browser_header_painter_ash.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/immersive_mode_controller.h"
#include "chrome/browser/ui/views/frame/web_app_left_header_view_ash.h"
#include "chrome/browser/ui/views/profiles/avatar_menu_button.h"
#include "chrome/browser/ui/views/tab_icon_view.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#include "chrome/browser/web_applications/web_app.h"
#include "components/signin/core/common/profile_management_switches.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/extension_registry.h"
#include "grit/theme_resources.h"
#include "ui/accessibility/ax_view_state.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window.h"
#include "ui/base/hit_test.h"
#include "ui/base/layout.h"
#include "ui/base/material_design/material_design_controller.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/theme_provider.h"
#include "ui/compositor/layer_animator.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/scoped_canvas.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/layout_constants.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"

namespace {

// Space between right edge of tabstrip and maximize button.
const int kTabstripRightSpacing = 10;
// Height of the shadow of the content area, at the top of the toolbar.
const int kContentShadowHeight = 1;
// Space between top of window and top of tabstrip for tall headers, such as
// for restored windows, apps, etc.
const int kTabstripTopSpacingTall = 7;
// Space between top of window and top of tabstrip for short headers, such as
// for maximized windows, pop-ups, etc.
const int kTabstripTopSpacingShort = 0;
// Height of the shadow in the tab image, used to ensure clicks in the shadow
// area still drag restored windows.  This keeps the clickable area large enough
// to hit easily.
const int kTabShadowHeight = 4;

// Combines View::ConvertPointToTarget() and View::HitTest() for a given
// |point|. Converts |point| from |src| to |dst| and hit tests it against |dst|.
bool ConvertedHitTest(views::View* src,
                      views::View* dst,
                      const gfx::Point& point) {
  DCHECK(src);
  DCHECK(dst);
  gfx::Point converted_point(point);
  views::View::ConvertPointToTarget(src, dst, &converted_point);
  return dst->HitTestPoint(converted_point);
}

}  // namespace

///////////////////////////////////////////////////////////////////////////////
// BrowserNonClientFrameViewAsh, public:

BrowserNonClientFrameViewAsh::BrowserNonClientFrameViewAsh(
    BrowserFrame* frame,
    BrowserView* browser_view)
    : BrowserNonClientFrameView(frame, browser_view),
      caption_button_container_(nullptr),
      web_app_left_header_view_(nullptr),
      window_icon_(nullptr),
      frame_border_hit_test_controller_(
          new ash::FrameBorderHitTestController(frame)) {
  ash::Shell::GetInstance()->AddShellObserver(this);
}

BrowserNonClientFrameViewAsh::~BrowserNonClientFrameViewAsh() {
  ash::Shell::GetInstance()->RemoveShellObserver(this);
}

void BrowserNonClientFrameViewAsh::Init() {
  caption_button_container_ = new ash::FrameCaptionButtonContainerView(frame());
  caption_button_container_->UpdateSizeButtonVisibility();
  AddChildView(caption_button_container_);

  // Initializing the TabIconView is expensive, so only do it if we need to.
  if (browser_view()->ShouldShowWindowIcon()) {
    window_icon_ = new TabIconView(this, nullptr);
    window_icon_->set_is_light(true);
    AddChildView(window_icon_);
    window_icon_->Update();
  }

  if (UsePackagedAppHeaderStyle() || UseWebAppHeaderStyle()) {
    ash::DefaultHeaderPainter* header_painter = new ash::DefaultHeaderPainter;
    header_painter_.reset(header_painter);
    header_painter->Init(frame(), this, caption_button_container_);
    if (UseWebAppHeaderStyle()) {
      web_app_left_header_view_ = new WebAppLeftHeaderView(browser_view());
      AddChildView(web_app_left_header_view_);
      header_painter->UpdateLeftHeaderView(web_app_left_header_view_);
    } else if (window_icon_) {
      header_painter->UpdateLeftHeaderView(window_icon_);
    }
  } else {
    BrowserHeaderPainterAsh* header_painter = new BrowserHeaderPainterAsh;
    header_painter_.reset(header_painter);
    header_painter->Init(frame(), browser_view(), this, window_icon_,
                         caption_button_container_);
  }
}

///////////////////////////////////////////////////////////////////////////////
// BrowserNonClientFrameView:

gfx::Rect BrowserNonClientFrameViewAsh::GetBoundsForTabStrip(
    views::View* tabstrip) const {
  if (!tabstrip)
    return gfx::Rect();

  // When the tab strip is painted in the immersive fullscreen light bar style,
  // the caption buttons and the avatar button are not visible. However, their
  // bounds are still used to compute the tab strip bounds so that the tabs have
  // the same horizontal position when the tab strip is painted in the immersive
  // light bar style as when the top-of-window views are revealed.
  const int left_inset = GetTabStripLeftInset();
  return gfx::Rect(left_inset, GetTopInset(false),
                   std::max(0, width() - left_inset - GetTabStripRightInset()),
                   tabstrip->GetPreferredSize().height());
}

int BrowserNonClientFrameViewAsh::GetTopInset(bool restored) const {
  if (!ShouldPaint() || UseImmersiveLightbarHeaderStyle())
    return 0;

  if (!browser_view()->IsTabStripVisible()) {
    return (UsePackagedAppHeaderStyle() || UseWebAppHeaderStyle())
        ? header_painter_->GetHeaderHeight()
        : caption_button_container_->bounds().bottom();
  }

  if (!ui::MaterialDesignController::IsModeMaterial()) {
    return ((frame()->IsMaximized() || frame()->IsFullscreen()) && !restored) ?
        kTabstripTopSpacingShort : kTabstripTopSpacingTall;
  }

  const int header_height = restored
      ? GetAshLayoutSize(
            AshLayoutSize::BROWSER_RESTORED_CAPTION_BUTTON).height()
      : header_painter_->GetHeaderHeight();
  return header_height - browser_view()->GetTabStripHeight();
}

int BrowserNonClientFrameViewAsh::GetThemeBackgroundXInset() const {
  return ash::HeaderPainterUtil::GetThemeBackgroundXInset();
}

void BrowserNonClientFrameViewAsh::UpdateThrobber(bool running) {
  if (window_icon_)
    window_icon_->Update();
}

void BrowserNonClientFrameViewAsh::UpdateToolbar() {
  if (web_app_left_header_view_)
    web_app_left_header_view_->Update();
}

views::View* BrowserNonClientFrameViewAsh::GetLocationIconView() const {
  return web_app_left_header_view_ ?
      web_app_left_header_view_->GetLocationIconView() : nullptr;
}

///////////////////////////////////////////////////////////////////////////////
// views::NonClientFrameView:

gfx::Rect BrowserNonClientFrameViewAsh::GetBoundsForClientView() const {
  // The ClientView must be flush with the top edge of the widget so that the
  // web contents can take up the entire screen in immersive fullscreen (with
  // or without the top-of-window views revealed). When in immersive fullscreen
  // and the top-of-window views are revealed, the TopContainerView paints the
  // window header by redirecting paints from its background to
  // BrowserNonClientFrameViewAsh.
  return bounds();
}

gfx::Rect BrowserNonClientFrameViewAsh::GetWindowBoundsForClientBounds(
    const gfx::Rect& client_bounds) const {
  return client_bounds;
}

int BrowserNonClientFrameViewAsh::NonClientHitTest(const gfx::Point& point) {
  const int hit_test = ash::FrameBorderHitTestController::NonClientHitTest(
      this, caption_button_container_, point);

  // See if the point is actually within the web app back button.
  if (hit_test == HTCAPTION && web_app_left_header_view_ &&
      ConvertedHitTest(this, web_app_left_header_view_, point)) {
    return HTCLIENT;
  }

  // When the window is restored we want a large click target above the tabs
  // to drag the window, so redirect clicks in the tab's shadow to caption.
  if (hit_test == HTCLIENT && !frame()->IsMaximized() &&
      !frame()->IsFullscreen()) {
    gfx::Point client_point(point);
    View::ConvertPointToTarget(this, frame()->client_view(), &client_point);
    gfx::Rect tabstrip_bounds(browser_view()->tabstrip()->bounds());
    if (client_point.y() < tabstrip_bounds.y() + kTabShadowHeight)
      return HTCAPTION;
  }

  return hit_test;
}

void BrowserNonClientFrameViewAsh::GetWindowMask(const gfx::Size& size,
                                                 gfx::Path* window_mask) {
  // Aura does not use window masks.
}

void BrowserNonClientFrameViewAsh::ResetWindowControls() {
  // Hide the caption buttons in immersive fullscreen when the tab light bar
  // is visible because it's confusing when the user hovers or clicks in the
  // top-right of the screen and hits one.
  caption_button_container_->SetVisible(!UseImmersiveLightbarHeaderStyle());
  caption_button_container_->ResetWindowControls();
}

void BrowserNonClientFrameViewAsh::UpdateWindowIcon() {
  if (window_icon_)
    window_icon_->SchedulePaint();
}

void BrowserNonClientFrameViewAsh::UpdateWindowTitle() {
  if (!frame()->IsFullscreen())
    header_painter_->SchedulePaintForTitle();
}

void BrowserNonClientFrameViewAsh::SizeConstraintsChanged() {
}

///////////////////////////////////////////////////////////////////////////////
// views::View:

void BrowserNonClientFrameViewAsh::OnPaint(gfx::Canvas* canvas) {
  if (!ShouldPaint())
    return;

  if (UseImmersiveLightbarHeaderStyle()) {
    // The light bar header is not themed because theming it does not look good.
    canvas->FillRect(
        gfx::Rect(width(), header_painter_->GetHeaderHeightForPainting()),
        SK_ColorBLACK);
    return;
  }

  const bool should_paint_as_active = ShouldPaintAsActive();
  caption_button_container_->SetPaintAsActive(should_paint_as_active);
  if (web_app_left_header_view_)
    web_app_left_header_view_->SetPaintAsActive(should_paint_as_active);

  const ash::HeaderPainter::Mode header_mode = should_paint_as_active ?
      ash::HeaderPainter::MODE_ACTIVE : ash::HeaderPainter::MODE_INACTIVE;
  header_painter_->PaintHeader(canvas, header_mode);

  if (browser_view()->IsToolbarVisible() &&
      !browser_view()->toolbar()->GetPreferredSize().IsEmpty() &&
      browser_view()->IsTabStripVisible()) {
    PaintToolbarBackground(canvas);
  }
}

void BrowserNonClientFrameViewAsh::Layout() {
  // The header must be laid out before computing |painted_height| because the
  // computation of |painted_height| for app and popup windows depends on the
  // position of the window controls.
  header_painter_->LayoutHeader();

  int painted_height = GetTopInset(false);
  if (browser_view()->IsTabStripVisible())
    painted_height += browser_view()->tabstrip()->GetPreferredSize().height();

  header_painter_->SetHeaderHeightForPainting(painted_height);

  if (avatar_button())
    LayoutAvatar();
  BrowserNonClientFrameView::Layout();
}

const char* BrowserNonClientFrameViewAsh::GetClassName() const {
  return "BrowserNonClientFrameViewAsh";
}

void BrowserNonClientFrameViewAsh::GetAccessibleState(ui::AXViewState* state) {
  state->role = ui::AX_ROLE_TITLE_BAR;
}

gfx::Size BrowserNonClientFrameViewAsh::GetMinimumSize() const {
  gfx::Size min_client_view_size(frame()->client_view()->GetMinimumSize());
  int min_width = std::max(header_painter_->GetMinimumHeaderWidth(),
                           min_client_view_size.width());
  if (browser_view()->IsTabStripVisible()) {
    // Ensure that the minimum width is enough to hold a minimum width tab strip
    // at its usual insets.
    const int min_tabstrip_width =
        browser_view()->tabstrip()->GetMinimumSize().width();
    min_width = std::max(
        min_width,
        min_tabstrip_width + GetTabStripLeftInset() + GetTabStripRightInset());
  }
  return gfx::Size(min_width, min_client_view_size.height());
}

void BrowserNonClientFrameViewAsh::ChildPreferredSizeChanged(
    views::View* child) {
  // FrameCaptionButtonContainerView animates the visibility changes in
  // UpdateSizeButtonVisibility(false). Due to this a new size is not available
  // until the completion of the animation. Layout in response to the preferred
  // size changes.
  if (browser_view()->initialized() && (child == caption_button_container_)) {
    InvalidateLayout();
    frame()->GetRootView()->Layout();
  }
}

///////////////////////////////////////////////////////////////////////////////
// ash::ShellObserver:

void BrowserNonClientFrameViewAsh::OnMaximizeModeStarted() {
  caption_button_container_->UpdateSizeButtonVisibility();
  InvalidateLayout();
  frame()->client_view()->InvalidateLayout();
  frame()->GetRootView()->Layout();
}

void BrowserNonClientFrameViewAsh::OnMaximizeModeEnded() {
  OnMaximizeModeStarted();
}

///////////////////////////////////////////////////////////////////////////////
// TabIconViewModel:

bool BrowserNonClientFrameViewAsh::ShouldTabIconViewAnimate() const {
  // This function is queried during the creation of the window as the
  // TabIconView we host is initialized, so we need to null check the selected
  // WebContents because in this condition there is not yet a selected tab.
  content::WebContents* current_tab = browser_view()->GetActiveWebContents();
  return current_tab && current_tab->IsLoading();
}

gfx::ImageSkia BrowserNonClientFrameViewAsh::GetFaviconForTabIconView() {
  views::WidgetDelegate* delegate = frame()->widget_delegate();
  return delegate ? delegate->GetWindowIcon() : gfx::ImageSkia();
}

///////////////////////////////////////////////////////////////////////////////
// BrowserNonClientFrameViewAsh, protected:

// BrowserNonClientFrameView:
void BrowserNonClientFrameViewAsh::UpdateAvatar() {
  UpdateOldAvatarButton();
}

///////////////////////////////////////////////////////////////////////////////
// BrowserNonClientFrameViewAsh, private:

// views::NonClientFrameView:
bool BrowserNonClientFrameViewAsh::DoesIntersectRect(
    const views::View* target,
    const gfx::Rect& rect) const {
  CHECK_EQ(this, target);
  if (!views::ViewTargeterDelegate::DoesIntersectRect(this, rect)) {
    // |rect| is outside BrowserNonClientFrameViewAsh's bounds.
    return false;
  }

  if (!browser_view()->IsTabStripVisible()) {
    // Claim |rect| if it is above the top of the topmost client area view.
    return rect.y() < GetTopInset(false);
  }

  // Claim |rect| only if it is above the bottom of the tabstrip in a non-tab
  // portion. In particular, the avatar label/button is left of the tabstrip and
  // the window controls are right of the tabstrip.
  TabStrip* tabstrip = browser_view()->tabstrip();
  gfx::RectF rect_in_tabstrip_coords_f(rect);
  View::ConvertRectToTarget(this, tabstrip, &rect_in_tabstrip_coords_f);
  const gfx::Rect rect_in_tabstrip_coords(
      gfx::ToEnclosingRect(rect_in_tabstrip_coords_f));
  return (rect_in_tabstrip_coords.y() <= tabstrip->height()) &&
          (!tabstrip->HitTestRect(rect_in_tabstrip_coords) ||
          tabstrip->IsRectInWindowCaption(rect_in_tabstrip_coords));
}

int BrowserNonClientFrameViewAsh::GetTabStripLeftInset() const {
  const gfx::Insets insets(GetLayoutInsets(AVATAR_ICON));
  const int avatar_right =
      avatar_button() ? (insets.left() + GetOTRAvatarIcon().width()) : 0;
  return avatar_right + insets.right();
}

int BrowserNonClientFrameViewAsh::GetTabStripRightInset() const {
  return kTabstripRightSpacing +
      caption_button_container_->GetPreferredSize().width();
}

bool BrowserNonClientFrameViewAsh::UseImmersiveLightbarHeaderStyle() const {
  const ImmersiveModeController* const immersive_controller =
      browser_view()->immersive_mode_controller();
  return immersive_controller->IsEnabled() &&
         !immersive_controller->IsRevealed() &&
         browser_view()->IsTabStripVisible();
}

bool BrowserNonClientFrameViewAsh::UsePackagedAppHeaderStyle() const {
  // Use for non tabbed trusted source windows, e.g. Settings, as well as apps
  // that aren't using the newer WebApp style.
  const Browser* const browser = browser_view()->browser();
  return (!browser->is_type_tabbed() && browser->is_trusted_source()) ||
         (browser->is_app() && !UseWebAppHeaderStyle());
}

bool BrowserNonClientFrameViewAsh::UseWebAppHeaderStyle() const {
  return browser_view()->browser()->SupportsWindowFeature(
      Browser::FEATURE_WEBAPPFRAME);
}

void BrowserNonClientFrameViewAsh::LayoutAvatar() {
  DCHECK(avatar_button());
#if !defined(OS_CHROMEOS)
  // ChromeOS shows avatar on V1 app.
  DCHECK(browser_view()->IsTabStripVisible());
#endif

  const gfx::ImageSkia incognito_icon = GetOTRAvatarIcon();
  const gfx::Insets avatar_insets = GetLayoutInsets(AVATAR_ICON);
  const int avatar_bottom = GetTopInset(false) +
      browser_view()->GetTabStripHeight() - avatar_insets.bottom();
  int avatar_y = avatar_bottom - incognito_icon.height();
  if (!ui::MaterialDesignController::IsModeMaterial() &&
      browser_view()->IsTabStripVisible() &&
      (frame()->IsMaximized() || frame()->IsFullscreen())) {
    avatar_y = GetTopInset(false) + kContentShadowHeight;
  }

  // Hide the incognito icon in immersive fullscreen when the tab light bar is
  // visible because the header is too short for the icognito icon to be
  // recognizable.
  const bool avatar_visible = !UseImmersiveLightbarHeaderStyle();
  const int avatar_height = avatar_visible ? (avatar_bottom - avatar_y) : 0;
  avatar_button()->SetBounds(avatar_insets.left(), avatar_y,
                             incognito_icon.width(), avatar_height);
  avatar_button()->SetVisible(avatar_visible);
}

bool BrowserNonClientFrameViewAsh::ShouldPaint() const {
  if (!frame()->IsFullscreen())
    return true;

  // We need to paint when in immersive fullscreen and either:
  // - The top-of-window views are revealed.
  // - The lightbar style tabstrip is visible.
  ImmersiveModeController* immersive_mode_controller =
      browser_view()->immersive_mode_controller();
  return immersive_mode_controller->IsEnabled() &&
      (immersive_mode_controller->IsRevealed() ||
       UseImmersiveLightbarHeaderStyle());
}

void BrowserNonClientFrameViewAsh::PaintToolbarBackground(gfx::Canvas* canvas) {
  gfx::Rect toolbar_bounds(browser_view()->GetToolbarBounds());
  if (toolbar_bounds.IsEmpty())
    return;
  gfx::Point toolbar_origin(toolbar_bounds.origin());
  View::ConvertPointToTarget(browser_view(), this, &toolbar_origin);
  toolbar_bounds.set_origin(toolbar_origin);

  const ui::ThemeProvider* tp = GetThemeProvider();
  const gfx::ImageSkia* const bg = tp->GetImageSkiaNamed(IDR_THEME_TOOLBAR);
  const int x = toolbar_bounds.x();
  const int y = toolbar_bounds.y();
  const int bg_y = GetTopInset(false) + Tab::GetYInsetForActiveTabBackground();
  const int w = toolbar_bounds.width();
  const int h = toolbar_bounds.height();
  const SkColor separator_color =
      tp->GetColor(ThemeProperties::COLOR_TOOLBAR_BOTTOM_SEPARATOR);
  if (ui::MaterialDesignController::IsModeMaterial()) {
    // Background.  The top stroke is drawn above the toolbar bounds, so
    // unlike in the non-Material Design code below, we don't need to exclude
    // any region from having the background image drawn over it.
    if (tp->HasCustomImage(IDR_THEME_TOOLBAR)) {
      canvas->TileImageInt(*bg, x + GetThemeBackgroundXInset(), y - bg_y, x, y,
                           w, h);
    } else {
      canvas->FillRect(toolbar_bounds,
                       tp->GetColor(ThemeProperties::COLOR_TOOLBAR));
    }

    // Top stroke.
    gfx::Rect separator_rect(x, y, w, 0);
    gfx::ScopedCanvas scoped_canvas(canvas);
    gfx::Rect tabstrip_bounds(GetBoundsForTabStrip(browser_view()->tabstrip()));
    tabstrip_bounds.set_x(GetMirroredXForRect(tabstrip_bounds));
    canvas->sk_canvas()->clipRect(gfx::RectToSkRect(tabstrip_bounds),
                                  SkRegion::kDifference_Op);
    separator_rect.set_y(tabstrip_bounds.bottom());
    BrowserView::Paint1pxHorizontalLine(canvas, GetToolbarTopSeparatorColor(),
                                        separator_rect, true);

    // Toolbar/content separator.
    toolbar_bounds.Inset(kClientEdgeThickness, 0);
    BrowserView::Paint1pxHorizontalLine(canvas, separator_color, toolbar_bounds,
                                        true);
  } else {
    // Background.  The top stroke is drawn using the IDR_TOOLBAR_SHADE_TOP
    // image, which overlays the toolbar.  The top 2 px of this image is the
    // actual top stroke + shadow, and is partly transparent, so the toolbar
    // background shouldn't be drawn over it.
    const int kContentEdgeShadowThickness = 2;
    const int bg_dest_y = y + kContentEdgeShadowThickness;
    const int bottom = toolbar_bounds.bottom();
    canvas->TileImageInt(*bg, x + GetThemeBackgroundXInset(), bg_dest_y - bg_y,
                         x, bg_dest_y, w, bottom - bg_dest_y);

    const gfx::ImageSkia* const top =
        tp->GetImageSkiaNamed(IDR_TOOLBAR_SHADE_TOP);
    canvas->TileImageInt(*top, 0, 0, x, y, w, top->height());

    // Draw the "lightening" shade line around the edges of the toolbar.
    const gfx::ImageSkia* const left =
        tp->GetImageSkiaNamed(IDR_TOOLBAR_SHADE_LEFT);
    const int img_y = y + top->height();
    const int img_w = left->width();
    const int img_h = bottom - img_y;
    canvas->TileImageInt(*left, 0, 0, x + kClientEdgeThickness, img_y, img_w,
                         img_h);
    const gfx::ImageSkia* const right =
        tp->GetImageSkiaNamed(IDR_TOOLBAR_SHADE_RIGHT);
    // TODO(pkasting): The "2 *" part of this makes no sense to me.
    canvas->TileImageInt(*right, 0, 0, w - (2 * kClientEdgeThickness) - img_w,
                         img_y, img_w, img_h);

    // Toolbar/content separator.
    toolbar_bounds.Inset(kClientEdgeThickness, h - kClientEdgeThickness,
                         kClientEdgeThickness, 0);
    canvas->FillRect(toolbar_bounds, separator_color);
  }
}