// Copyright (c) 2016 The Chromium Embedded Framework 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 "tests/cefclient/browser/views_window.h"

#include <algorithm>

#include "include/base/cef_bind.h"
#include "include/base/cef_build.h"
#include "include/cef_app.h"
#include "include/views/cef_box_layout.h"
#include "include/wrapper/cef_helpers.h"
#include "tests/cefclient/browser/resource.h"
#include "tests/cefclient/browser/views_style.h"
#include "tests/shared/browser/resource_util.h"
#include "tests/shared/common/client_switches.h"

#if !defined(OS_WIN)
#define VK_RETURN 0x0D
#define VK_MENU 0x12  // ALT key.
#endif

namespace client {

namespace {

// Control IDs for Views in the top-level Window.
enum ControlIds {
  ID_WINDOW = 1,
  ID_BROWSER_VIEW,
  ID_BACK_BUTTON,
  ID_FORWARD_BUTTON,
  ID_STOP_BUTTON,
  ID_RELOAD_BUTTON,
  ID_URL_TEXTFIELD,
  ID_MENU_BUTTON,

  // Reserved range of top menu button IDs.
  ID_TOP_MENU_FIRST,
  ID_TOP_MENU_LAST = ID_TOP_MENU_FIRST + 10,
};

typedef std::vector<CefRefPtr<CefLabelButton>> LabelButtons;

// Make all |buttons| the same size.
void MakeButtonsSameSize(const LabelButtons& buttons) {
  CefSize size;

  // Determine the largest button size.
  for (size_t i = 0U; i < buttons.size(); ++i) {
    const CefSize& button_size = buttons[i]->GetPreferredSize();
    if (size.width < button_size.width)
      size.width = button_size.width;
    if (size.height < button_size.height)
      size.height = button_size.height;
  }

  for (size_t i = 0U; i < buttons.size(); ++i) {
    // Set the button's minimum size.
    buttons[i]->SetMinimumSize(size);

    // Re-layout the button and all parent Views.
    buttons[i]->InvalidateLayout();
  }
}

void AddTestMenuItems(CefRefPtr<CefMenuModel> test_menu) {
  test_menu->AddItem(ID_TESTS_GETSOURCE, "Get Source");
  test_menu->AddItem(ID_TESTS_GETTEXT, "Get Text");
  test_menu->AddItem(ID_TESTS_WINDOW_NEW, "New Window");
  test_menu->AddItem(ID_TESTS_WINDOW_POPUP, "Popup Window");
  test_menu->AddItem(ID_TESTS_REQUEST, "Request");
  test_menu->AddItem(ID_TESTS_PLUGIN_INFO, "Plugin Info");
  test_menu->AddItem(ID_TESTS_ZOOM_IN, "Zoom In");
  test_menu->AddItem(ID_TESTS_ZOOM_OUT, "Zoom Out");
  test_menu->AddItem(ID_TESTS_ZOOM_RESET, "Zoom Reset");
  test_menu->AddItem(ID_TESTS_TRACING_BEGIN, "Begin Tracing");
  test_menu->AddItem(ID_TESTS_TRACING_END, "End Tracing");
  test_menu->AddItem(ID_TESTS_PRINT, "Print");
  test_menu->AddItem(ID_TESTS_PRINT_TO_PDF, "Print to PDF");
  test_menu->AddItem(ID_TESTS_OTHER_TESTS, "Other Tests");
}

void AddFileMenuItems(CefRefPtr<CefMenuModel> file_menu) {
  file_menu->AddItem(ID_QUIT, "E&xit");

  // Show the accelerator shortcut text in the menu.
  file_menu->SetAcceleratorAt(file_menu->GetCount() - 1, 'X', false, false,
                              true);
}

}  // namespace

// static
CefRefPtr<ViewsWindow> ViewsWindow::Create(
    Delegate* delegate,
    CefRefPtr<CefClient> client,
    const CefString& url,
    const CefBrowserSettings& settings,
    CefRefPtr<CefRequestContext> request_context) {
  CEF_REQUIRE_UI_THREAD();
  DCHECK(delegate);

  // Create a new ViewsWindow.
  CefRefPtr<ViewsWindow> views_window = new ViewsWindow(delegate, NULL);

  // Create a new BrowserView.
  CefRefPtr<CefBrowserView> browser_view = CefBrowserView::CreateBrowserView(
      client, url, settings, request_context, views_window);

  // Associate the BrowserView with the ViewsWindow.
  views_window->SetBrowserView(browser_view);

  // Create a new top-level Window. It will show itself after creation.
  CefWindow::CreateTopLevelWindow(views_window);

  return views_window;
}

void ViewsWindow::Show() {
  CEF_REQUIRE_UI_THREAD();
  if (window_)
    window_->Show();
}

void ViewsWindow::Hide() {
  CEF_REQUIRE_UI_THREAD();
  if (window_)
    window_->Hide();
}

void ViewsWindow::Minimize() {
  CEF_REQUIRE_UI_THREAD();
  if (window_)
    window_->Minimize();
}

void ViewsWindow::Maximize() {
  CEF_REQUIRE_UI_THREAD();
  if (window_)
    window_->Maximize();
}

void ViewsWindow::SetBounds(const CefRect& bounds) {
  CEF_REQUIRE_UI_THREAD();
  if (window_)
    window_->SetBounds(bounds);
}

void ViewsWindow::Close(bool force) {
  CEF_REQUIRE_UI_THREAD();
  if (!browser_view_)
    return;

  CefRefPtr<CefBrowser> browser = browser_view_->GetBrowser();
  if (browser) {
    // This will result in a call to CefWindow::Close() which will then call
    // ViewsWindow::CanClose().
    browser->GetHost()->CloseBrowser(force);
  }
}

void ViewsWindow::SetAddress(const std::string& url) {
  CEF_REQUIRE_UI_THREAD();
  if (!window_ || !with_controls_)
    return;

  CefRefPtr<CefView> view = window_->GetViewForID(ID_URL_TEXTFIELD);
  if (view && view->AsTextfield())
    view->AsTextfield()->SetText(url);
}

void ViewsWindow::SetTitle(const std::string& title) {
  CEF_REQUIRE_UI_THREAD();
  if (window_)
    window_->SetTitle(title);
}

void ViewsWindow::SetFavicon(CefRefPtr<CefImage> image) {
  CEF_REQUIRE_UI_THREAD();

  // Window icons should be 16 DIP in size.
  DCHECK_EQ(std::max(image->GetWidth(), image->GetHeight()), 16U);

  if (window_)
    window_->SetWindowIcon(image);
}

void ViewsWindow::SetFullscreen(bool fullscreen) {
  CEF_REQUIRE_UI_THREAD();
  if (window_) {
    // Hide the top controls while in full-screen mode.
    if (with_controls_)
      ShowTopControls(!fullscreen);

    window_->SetFullscreen(fullscreen);
  }
}

void ViewsWindow::SetLoadingState(bool isLoading,
                                  bool canGoBack,
                                  bool canGoForward) {
  CEF_REQUIRE_UI_THREAD();
  if (!window_ || !with_controls_)
    return;

  EnableView(ID_BACK_BUTTON, canGoBack);
  EnableView(ID_FORWARD_BUTTON, canGoForward);
  EnableView(ID_RELOAD_BUTTON, !isLoading);
  EnableView(ID_STOP_BUTTON, isLoading);
  EnableView(ID_URL_TEXTFIELD, true);
}

void ViewsWindow::SetDraggableRegions(
    const std::vector<CefDraggableRegion>& regions) {
  CEF_REQUIRE_UI_THREAD();

  if (!window_ || !browser_view_)
    return;

  std::vector<CefDraggableRegion> window_regions;

  // Convert the regions from BrowserView to Window coordinates.
  std::vector<CefDraggableRegion>::const_iterator it = regions.begin();
  for (; it != regions.end(); ++it) {
    CefDraggableRegion region = *it;
    CefPoint origin = CefPoint(region.bounds.x, region.bounds.y);
    browser_view_->ConvertPointToWindow(origin);
    region.bounds.x = origin.x;
    region.bounds.y = origin.y;
    window_regions.push_back(region);
  }

  window_->SetDraggableRegions(window_regions);
}

void ViewsWindow::TakeFocus(bool next) {
  CEF_REQUIRE_UI_THREAD();

  if (!window_ || !with_controls_)
    return;

  // Give focus to the URL textfield.
  window_->GetViewForID(ID_URL_TEXTFIELD)->RequestFocus();
}

void ViewsWindow::OnBeforeContextMenu(CefRefPtr<CefMenuModel> model) {
  CEF_REQUIRE_UI_THREAD();

  views_style::ApplyTo(model);
}

bool ViewsWindow::OnPopupBrowserViewCreated(
    CefRefPtr<CefBrowserView> browser_view,
    CefRefPtr<CefBrowserView> popup_browser_view,
    bool is_devtools) {
  CEF_REQUIRE_UI_THREAD();

  // The popup browser client is created in CefLifeSpanHandler::OnBeforePopup()
  // (e.g. via RootWindowViews::InitAsPopup()). The Delegate (RootWindowViews)
  // knows the association between |client| and itself.
  Delegate* popup_delegate = delegate_->GetDelegateForPopup(
      popup_browser_view->GetBrowser()->GetHost()->GetClient());

  // Create a new ViewsWindow for the popup BrowserView.
  CefRefPtr<ViewsWindow> popup_window =
      new ViewsWindow(popup_delegate, popup_browser_view);

  // Create a new top-level Window for the popup. It will show itself after
  // creation.
  CefWindow::CreateTopLevelWindow(popup_window);

  // We created the Window.
  return true;
}

void ViewsWindow::OnButtonPressed(CefRefPtr<CefButton> button) {
  CEF_REQUIRE_UI_THREAD();
  DCHECK(with_controls_);

  if (!browser_view_)
    return;

  CefRefPtr<CefBrowser> browser = browser_view_->GetBrowser();
  if (!browser)
    return;

  switch (button->GetID()) {
    case ID_BACK_BUTTON:
      browser->GoBack();
      break;
    case ID_FORWARD_BUTTON:
      browser->GoForward();
      break;
    case ID_STOP_BUTTON:
      browser->StopLoad();
      break;
    case ID_RELOAD_BUTTON:
      browser->Reload();
      break;
    case ID_MENU_BUTTON:
      break;
    default:
      NOTREACHED();
      break;
  }
}

void ViewsWindow::OnMenuButtonPressed(CefRefPtr<CefMenuButton> menu_button,
                                      const CefPoint& screen_point) {
  CEF_REQUIRE_UI_THREAD();
  DCHECK(with_controls_);
  DCHECK_EQ(ID_MENU_BUTTON, menu_button->GetID());

  menu_button->ShowMenu(button_menu_model_, screen_point,
                        CEF_MENU_ANCHOR_TOPRIGHT);
}

void ViewsWindow::ExecuteCommand(CefRefPtr<CefMenuModel> menu_model,
                                 int command_id,
                                 cef_event_flags_t event_flags) {
  CEF_REQUIRE_UI_THREAD();
  DCHECK(with_controls_);

  if (command_id == ID_QUIT) {
    delegate_->OnExit();
  } else if (command_id >= ID_TESTS_FIRST && command_id <= ID_TESTS_LAST) {
    delegate_->OnTest(command_id);
  } else {
    NOTREACHED();
  }
}

bool ViewsWindow::OnKeyEvent(CefRefPtr<CefTextfield> textfield,
                             const CefKeyEvent& event) {
  CEF_REQUIRE_UI_THREAD();
  DCHECK(with_controls_);
  DCHECK_EQ(ID_URL_TEXTFIELD, textfield->GetID());

  // Trigger when the return key is pressed.
  if (window_ && browser_view_ && event.type == KEYEVENT_RAWKEYDOWN &&
      event.windows_key_code == VK_RETURN) {
    CefRefPtr<CefBrowser> browser = browser_view_->GetBrowser();
    if (browser) {
      CefRefPtr<CefView> view = window_->GetViewForID(ID_URL_TEXTFIELD);
      if (view && view->AsTextfield()) {
        const CefString& url = view->AsTextfield()->GetText();
        if (!url.empty())
          browser->GetMainFrame()->LoadURL(url);
      }
    }

    // We handled the event.
    return true;
  }

  return false;
}

void ViewsWindow::OnWindowCreated(CefRefPtr<CefWindow> window) {
  CEF_REQUIRE_UI_THREAD();
  DCHECK(browser_view_);
  DCHECK(!window_);
  DCHECK(window);

  window_ = window;
  window_->SetID(ID_WINDOW);

  with_controls_ = delegate_->WithControls();

  delegate_->OnViewsWindowCreated(this);

  CefRect bounds = delegate_->GetWindowBounds();
  if (bounds.IsEmpty()) {
    // Use the default size.
    bounds.width = 800;
    bounds.height = 600;
  }

  if (bounds.x == 0 && bounds.y == 0) {
    // Size the Window and center it.
    window_->CenterWindow(CefSize(bounds.width, bounds.height));
  } else {
    // Set the Window bounds as specified.
    window_->SetBounds(bounds);
  }

  // Set the background color for regions that are not obscured by other Views.
  views_style::ApplyTo(window_.get());

  if (with_controls_) {
    // Add the BrowserView and other controls to the Window.
    AddControls();

    // Add keyboard accelerators to the Window.
    AddAccelerators();
  } else {
    // Add the BrowserView as the only child of the Window.
    window_->AddChildView(browser_view_);

    // Choose a reasonable minimum window size.
    minimum_window_size_ = CefSize(100, 100);
  }

  // Show the Window.
  window_->Show();

  // Give keyboard focus to the BrowserView.
  browser_view_->RequestFocus();
}

void ViewsWindow::OnWindowDestroyed(CefRefPtr<CefWindow> window) {
  CEF_REQUIRE_UI_THREAD();
  DCHECK(window_);

  delegate_->OnViewsWindowDestroyed(this);

  browser_view_ = NULL;
  button_menu_model_ = NULL;
  if (top_menu_bar_) {
    top_menu_bar_->Reset();
    top_menu_bar_ = NULL;
  }
  window_ = NULL;
}

bool ViewsWindow::CanClose(CefRefPtr<CefWindow> window) {
  CEF_REQUIRE_UI_THREAD();

  // Allow the window to close if the browser says it's OK.
  CefRefPtr<CefBrowser> browser = browser_view_->GetBrowser();
  if (browser)
    return browser->GetHost()->TryCloseBrowser();
  return true;
}

bool ViewsWindow::IsFrameless(CefRefPtr<CefWindow> window) {
  CEF_REQUIRE_UI_THREAD();
  return frameless_;
}

bool ViewsWindow::OnAccelerator(CefRefPtr<CefWindow> window, int command_id) {
  CEF_REQUIRE_UI_THREAD();

  if (command_id == ID_QUIT) {
    delegate_->OnExit();
    return true;
  }

  return false;
}

bool ViewsWindow::OnKeyEvent(CefRefPtr<CefWindow> window,
                             const CefKeyEvent& event) {
  CEF_REQUIRE_UI_THREAD();

  if (!window_ || !with_controls_)
    return false;

  if (event.type == KEYEVENT_RAWKEYDOWN && event.windows_key_code == VK_MENU) {
    // ALT key is pressed.
    int last_focused_view = last_focused_view_;
    bool menu_had_focus = menu_has_focus_;

    // Toggle menu button focusable.
    SetMenuFocusable(!menu_has_focus_);

    if (menu_had_focus && last_focused_view != 0) {
      // Restore focus to the view that was previously focused.
      window_->GetViewForID(last_focused_view)->RequestFocus();
    }

    return true;
  }

  if (menu_has_focus_ && top_menu_bar_)
    return top_menu_bar_->OnKeyEvent(event);

  return false;
}

CefSize ViewsWindow::GetMinimumSize(CefRefPtr<CefView> view) {
  CEF_REQUIRE_UI_THREAD();

  if (view->GetID() == ID_WINDOW)
    return minimum_window_size_;

  return CefSize();
}

void ViewsWindow::OnFocus(CefRefPtr<CefView> view) {
  CEF_REQUIRE_UI_THREAD();

  const int view_id = view->GetID();

  // Keep track of the non-menu view that was last focused.
  if (last_focused_view_ != view_id &&
      (!top_menu_bar_ || !top_menu_bar_->HasMenuId(view_id))) {
    last_focused_view_ = view_id;
  }

  // When focus leaves the menu buttons make them unfocusable.
  if (menu_has_focus_) {
    if (top_menu_bar_) {
      if (!top_menu_bar_->HasMenuId(view_id))
        SetMenuFocusable(false);
    } else if (view_id != ID_MENU_BUTTON) {
      SetMenuFocusable(false);
    }
  }
}

void ViewsWindow::MenuBarExecuteCommand(CefRefPtr<CefMenuModel> menu_model,
                                        int command_id,
                                        cef_event_flags_t event_flags) {
  ExecuteCommand(menu_model, command_id, event_flags);
}

ViewsWindow::ViewsWindow(Delegate* delegate,
                         CefRefPtr<CefBrowserView> browser_view)
    : delegate_(delegate),
      with_controls_(false),
      menu_has_focus_(false),
      last_focused_view_(false) {
  DCHECK(delegate_);
  if (browser_view)
    SetBrowserView(browser_view);

  CefRefPtr<CefCommandLine> command_line =
      CefCommandLine::GetGlobalCommandLine();
  frameless_ = command_line->HasSwitch(switches::kHideFrame);

  if (!command_line->HasSwitch(switches::kHideTopMenu)) {
    top_menu_bar_ = new ViewsMenuBar(this, ID_TOP_MENU_FIRST);
  }
}

void ViewsWindow::SetBrowserView(CefRefPtr<CefBrowserView> browser_view) {
  DCHECK(!browser_view_);
  DCHECK(browser_view);
  DCHECK(browser_view->IsValid());
  DCHECK(!browser_view->IsAttached());
  browser_view_ = browser_view;
  browser_view_->SetID(ID_BROWSER_VIEW);
}

void ViewsWindow::CreateMenuModel() {
  // Create the menu button model.
  button_menu_model_ = CefMenuModel::CreateMenuModel(this);
  CefRefPtr<CefMenuModel> test_menu =
      button_menu_model_->AddSubMenu(0, "&Tests");
  views_style::ApplyTo(button_menu_model_);
  AddTestMenuItems(test_menu);
  AddFileMenuItems(button_menu_model_);

  if (top_menu_bar_) {
    // Add the menus to the top menu bar.
    AddFileMenuItems(top_menu_bar_->CreateMenuModel("&File", NULL));
    AddTestMenuItems(top_menu_bar_->CreateMenuModel("&Tests", NULL));
  }
}

CefRefPtr<CefLabelButton> ViewsWindow::CreateBrowseButton(
    const std::string& label,
    int id) {
  // The default framed button image resources (IDR_BUTTON_*) look pretty bad
  // with non-default background colors, so we'll use frameless buttons with
  // ink drop when a background color is specified.
  const bool with_frame = !views_style::IsSet();

  CefRefPtr<CefLabelButton> button =
      CefLabelButton::CreateLabelButton(this, label, with_frame);
  button->SetID(id);
  button->SetEnabled(false);    // Disabled by default.
  button->SetFocusable(false);  // Don't give focus to the button.

  if (!with_frame) {
    views_style::ApplyTo(button);
    button->SetInkDropEnabled(true);
    button->SetHorizontalAlignment(CEF_HORIZONTAL_ALIGNMENT_CENTER);
  }

  return button;
}

void ViewsWindow::AddControls() {
  // Create the MenuModel that will be displayed via the menu button.
  CreateMenuModel();

  CefRefPtr<CefPanel> top_menu_panel;
  if (top_menu_bar_)
    top_menu_panel = top_menu_bar_->GetMenuPanel();

  // Create the browse buttons.
  LabelButtons browse_buttons;
  browse_buttons.push_back(CreateBrowseButton("Back", ID_BACK_BUTTON));
  browse_buttons.push_back(CreateBrowseButton("Forward", ID_FORWARD_BUTTON));
  browse_buttons.push_back(CreateBrowseButton("Reload", ID_RELOAD_BUTTON));
  browse_buttons.push_back(CreateBrowseButton("Stop", ID_STOP_BUTTON));

  // Create the URL textfield.
  CefRefPtr<CefTextfield> url_textfield = CefTextfield::CreateTextfield(this);
  url_textfield->SetID(ID_URL_TEXTFIELD);
  url_textfield->SetEnabled(false);  // Disabled by default.
  views_style::ApplyTo(url_textfield);

  // Create the menu button.
  CefRefPtr<CefMenuButton> menu_button =
      CefMenuButton::CreateMenuButton(this, CefString(), false, false);
  menu_button->SetID(ID_MENU_BUTTON);
  menu_button->SetImage(CEF_BUTTON_STATE_NORMAL, LoadImageIcon("menu_icon"));
  views_style::ApplyTo(menu_button.get());
  menu_button->SetInkDropEnabled(true);
  // Override the default minimum size.
  menu_button->SetMinimumSize(CefSize(0, 0));

  // Create the top panel.
  CefRefPtr<CefPanel> top_panel = CefPanel::CreatePanel(NULL);

  // Use a horizontal box layout for |top_panel|.
  CefBoxLayoutSettings top_panel_layout_settings;
  top_panel_layout_settings.horizontal = true;
  CefRefPtr<CefBoxLayout> top_panel_layout =
      top_panel->SetToBoxLayout(top_panel_layout_settings);

  // Add the buttons and URL textfield to |top_panel|.
  for (size_t i = 0U; i < browse_buttons.size(); ++i)
    top_panel->AddChildView(browse_buttons[i]);
  top_panel->AddChildView(url_textfield);
  top_panel->AddChildView(menu_button);
  views_style::ApplyTo(top_panel);

  // Allow |url_textfield| to grow and fill any remaining space.
  top_panel_layout->SetFlexForView(url_textfield, 1);

  // Use a vertical box layout for |window|.
  CefBoxLayoutSettings window_layout_settings;
  window_layout_settings.horizontal = false;
  window_layout_settings.between_child_spacing = 2;
  CefRefPtr<CefBoxLayout> window_layout =
      window_->SetToBoxLayout(window_layout_settings);

  // Add the top panel and browser view to |window|.
  if (top_menu_panel)
    window_->AddChildView(top_menu_panel);
  window_->AddChildView(top_panel);
  window_->AddChildView(browser_view_);

  // Allow |browser_view_| to grow and fill any remaining space.
  window_layout->SetFlexForView(browser_view_, 1);

  // Lay out |window| so we can get the default button sizes.
  window_->Layout();

  // Make all browse buttons the same size.
  MakeButtonsSameSize(browse_buttons);

  // Lay out |window| again with the new button sizes.
  window_->Layout();

  // Minimum window width is the size of all buttons plus some extra.
  const int min_width = browse_buttons[0]->GetBounds().width * 4 +
                        menu_button->GetBounds().width + 100;
  // Minimum window height is the hight of the top toolbar plus some extra.
  int min_height = top_panel->GetBounds().height + 100;
  if (top_menu_panel)
    min_height += top_menu_panel->GetBounds().height;

  minimum_window_size_ = CefSize(min_width, min_height);
}

void ViewsWindow::AddAccelerators() {
  // Trigger accelerators without first forwarding to web content.
  browser_view_->SetPreferAccelerators(true);

  // Specify the accelerators to handle. OnAccelerator will be called when the
  // accelerator is triggered.
  window_->SetAccelerator(ID_QUIT, 'X', false, false, true);
}

void ViewsWindow::SetMenuFocusable(bool focusable) {
  if (!window_ || !with_controls_)
    return;

  if (top_menu_bar_) {
    top_menu_bar_->SetMenuFocusable(focusable);
  } else {
    window_->GetViewForID(ID_MENU_BUTTON)->SetFocusable(focusable);

    if (focusable) {
      // Give focus to menu button.
      window_->GetViewForID(ID_MENU_BUTTON)->RequestFocus();
    }
  }

  menu_has_focus_ = focusable;
}

void ViewsWindow::EnableView(int id, bool enable) {
  if (!window_)
    return;
  CefRefPtr<CefView> view = window_->GetViewForID(id);
  if (view)
    view->SetEnabled(enable);
}

void ViewsWindow::ShowTopControls(bool show) {
  if (!window_ || !with_controls_)
    return;

  // Change the visibility of the panel that contains the buttons.
  CefRefPtr<CefView> parent_view =
      window_->GetViewForID(ID_BACK_BUTTON)->GetParentView();
  if (parent_view->IsVisible() != show) {
    parent_view->SetVisible(show);
    parent_view->InvalidateLayout();
  }
}

}  // namespace client
