// Copyright (c) 2013 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 "ui/keyboard/content/keyboard_ui_content.h"

#include "base/command_line.h"
#include "base/macros.h"
#include "base/values.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/render_widget_host_iterator.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/browser/web_ui.h"
#include "content/public/common/bindings_policy.h"
#include "ui/aura/layout_manager.h"
#include "ui/aura/window.h"
#include "ui/base/ime/input_method.h"
#include "ui/base/ime/text_input_client.h"
#include "ui/keyboard/content/keyboard_constants.h"
#include "ui/keyboard/keyboard_controller.h"
#include "ui/keyboard/keyboard_switches.h"
#include "ui/keyboard/keyboard_util.h"
#include "ui/wm/core/shadow.h"

namespace {

// The WebContentsDelegate for the keyboard.
// The delegate deletes itself when the keyboard is destroyed.
class KeyboardContentsDelegate : public content::WebContentsDelegate,
                                 public content::WebContentsObserver {
 public:
  explicit KeyboardContentsDelegate(keyboard::KeyboardUIContent* ui)
      : ui_(ui) {}
  ~KeyboardContentsDelegate() override {}

 private:
  // Overridden from content::WebContentsDelegate:
  content::WebContents* OpenURLFromTab(
      content::WebContents* source,
      const content::OpenURLParams& params) override {
    source->GetController().LoadURL(
        params.url, params.referrer, params.transition, params.extra_headers);
    Observe(source);
    return source;
  }

  bool CanDragEnter(content::WebContents* source,
                    const content::DropData& data,
                    blink::WebDragOperationsMask operations_allowed) override {
    return false;
  }

  bool ShouldCreateWebContents(
      content::WebContents* web_contents,
      int route_id,
      int main_frame_route_id,
      int main_frame_widget_route_id,
      WindowContainerType window_container_type,
      const std::string& frame_name,
      const GURL& target_url,
      const std::string& partition_id,
      content::SessionStorageNamespace* session_storage_namespace) override {
    return false;
  }

  bool IsPopupOrPanel(const content::WebContents* source) const override {
    return true;
  }

  void MoveContents(content::WebContents* source,
                    const gfx::Rect& pos) override {
    aura::Window* keyboard = ui_->GetKeyboardWindow();
    // keyboard window must have been added to keyboard container window at this
    // point. Otherwise, wrong keyboard bounds is used and may cause problem as
    // described in crbug.com/367788.
    DCHECK(keyboard->parent());
    // keyboard window bounds may not set to |pos| after this call. If keyboard
    // is in FULL_WIDTH mode, only the height of keyboard window will be
    // changed.
    keyboard->SetBounds(pos);
  }

  // Overridden from content::WebContentsDelegate:
  void RequestMediaAccessPermission(
      content::WebContents* web_contents,
      const content::MediaStreamRequest& request,
      const content::MediaResponseCallback& callback) override {
    ui_->RequestAudioInput(web_contents, request, callback);
  }

  // Overridden from content::WebContentsObserver:
  void WebContentsDestroyed() override { delete this; }

  keyboard::KeyboardUIContent* ui_;

  DISALLOW_COPY_AND_ASSIGN(KeyboardContentsDelegate);
};

}  // namespace

namespace keyboard {

class WindowBoundsChangeObserver : public aura::WindowObserver {
 public:
  explicit WindowBoundsChangeObserver(KeyboardUIContent* ui) : ui_(ui) {}
  ~WindowBoundsChangeObserver() override {}

  void AddObservedWindow(aura::Window* window) {
    if (!window->HasObserver(this)) {
      window->AddObserver(this);
      observed_windows_.insert(window);
    }
  }
  void RemoveAllObservedWindows() {
    for (std::set<aura::Window*>::iterator it = observed_windows_.begin();
         it != observed_windows_.end(); ++it)
      (*it)->RemoveObserver(this);
    observed_windows_.clear();
  }

 private:
  void OnWindowBoundsChanged(aura::Window* window,
                             const gfx::Rect& old_bounds,
                             const gfx::Rect& new_bounds) override {
    ui_->UpdateInsetsForWindow(window);
  }
  void OnWindowDestroyed(aura::Window* window) override {
    if (window->HasObserver(this))
      window->RemoveObserver(this);
    observed_windows_.erase(window);
  }

  KeyboardUIContent* ui_;
  std::set<aura::Window*> observed_windows_;

  DISALLOW_COPY_AND_ASSIGN(WindowBoundsChangeObserver);
};

KeyboardUIContent::KeyboardUIContent(content::BrowserContext* context)
    : browser_context_(context),
      default_url_(kKeyboardURL),
      window_bounds_observer_(new WindowBoundsChangeObserver(this)) {
}

KeyboardUIContent::~KeyboardUIContent() {
  ResetInsets();
}

void KeyboardUIContent::LoadSystemKeyboard() {
  DCHECK(keyboard_contents_);
  if (keyboard_contents_->GetURL() != default_url_) {
    // TODO(bshe): The height of system virtual keyboard and IME virtual
    // keyboard may different. The height needs to be restored too.
    LoadContents(default_url_);
  }
}

void KeyboardUIContent::UpdateInsetsForWindow(aura::Window* window) {
  aura::Window* keyboard_container =
      keyboard_controller()->GetContainerWindow();
  if (!ShouldWindowOverscroll(window))
    return;

  std::unique_ptr<content::RenderWidgetHostIterator> widgets(
      content::RenderWidgetHost::GetRenderWidgetHosts());
  while (content::RenderWidgetHost* widget = widgets->GetNextHost()) {
    content::RenderWidgetHostView* view = widget->GetView();
    if (view && window->Contains(view->GetNativeView())) {
      gfx::Rect window_bounds = view->GetNativeView()->GetBoundsInScreen();
      gfx::Rect intersect =
          gfx::IntersectRects(window_bounds, keyboard_container->bounds());
      int overlap = ShouldEnableInsets(window) ? intersect.height() : 0;
      if (overlap > 0 && overlap < window_bounds.height())
        view->SetInsets(gfx::Insets(0, 0, overlap, 0));
      else
        view->SetInsets(gfx::Insets());
      return;
    }
  }
}

aura::Window* KeyboardUIContent::GetKeyboardWindow() {
  if (!keyboard_contents_) {
    content::BrowserContext* context = browser_context();
    keyboard_contents_.reset(content::WebContents::Create(
        content::WebContents::CreateParams(context,
            content::SiteInstance::CreateForURL(context,
                                                GetVirtualKeyboardUrl()))));
    keyboard_contents_->SetDelegate(new KeyboardContentsDelegate(this));
    SetupWebContents(keyboard_contents_.get());
    LoadContents(GetVirtualKeyboardUrl());
    keyboard_contents_->GetNativeView()->AddObserver(this);
  }

  return keyboard_contents_->GetNativeView();
}

bool KeyboardUIContent::HasKeyboardWindow() const {
  return !!keyboard_contents_;
}

bool KeyboardUIContent::ShouldWindowOverscroll(aura::Window* window) const {
  return true;
}

void KeyboardUIContent::ReloadKeyboardIfNeeded() {
  DCHECK(keyboard_contents_);
  if (keyboard_contents_->GetURL() != GetVirtualKeyboardUrl()) {
    if (keyboard_contents_->GetURL().GetOrigin() !=
        GetVirtualKeyboardUrl().GetOrigin()) {
      // Sets keyboard window rectangle to 0 and close current page before
      // navigate to a keyboard in a different extension. This keeps the UX the
      // same as Android. Note we need to explicitly close current page as it
      // might try to resize keyboard window in javascript on a resize event.
      GetKeyboardWindow()->SetBounds(gfx::Rect());
      keyboard_contents_->ClosePage();
      keyboard_controller()->SetKeyboardMode(FULL_WIDTH);
    }
    LoadContents(GetVirtualKeyboardUrl());
  }
}

void KeyboardUIContent::InitInsets(const gfx::Rect& new_bounds) {
  // Adjust the height of the viewport for visible windows on the primary
  // display.
  // TODO(kevers): Add EnvObserver to properly initialize insets if a
  // window is created while the keyboard is visible.
  std::unique_ptr<content::RenderWidgetHostIterator> widgets(
      content::RenderWidgetHost::GetRenderWidgetHosts());
  while (content::RenderWidgetHost* widget = widgets->GetNextHost()) {
    content::RenderWidgetHostView* view = widget->GetView();
    // Can be NULL, e.g. if the RenderWidget is being destroyed or
    // the render process crashed.
    if (view) {
      aura::Window* window = view->GetNativeView();
      // Added while we determine if RenderWidgetHostViewChildFrame can be
      // changed to always return a non-null value: https://crbug.com/644726 .
      // If we cannot guarantee a non-null value, then this may need to stay.
      if (!window)
        continue;

      if (ShouldWindowOverscroll(window)) {
        gfx::Rect window_bounds = window->GetBoundsInScreen();
        gfx::Rect intersect = gfx::IntersectRects(window_bounds,
                                                  new_bounds);
        int overlap = intersect.height();
        if (overlap > 0 && overlap < window_bounds.height())
          view->SetInsets(gfx::Insets(0, 0, overlap, 0));
        else
          view->SetInsets(gfx::Insets());
        AddBoundsChangedObserver(window);
      }
    }
  }
}

void KeyboardUIContent::ResetInsets() {
  const gfx::Insets insets;
  std::unique_ptr<content::RenderWidgetHostIterator> widgets(
      content::RenderWidgetHost::GetRenderWidgetHosts());
  while (content::RenderWidgetHost* widget = widgets->GetNextHost()) {
    content::RenderWidgetHostView* view = widget->GetView();
    if (view)
      view->SetInsets(insets);
  }
  window_bounds_observer_->RemoveAllObservedWindows();
}

void KeyboardUIContent::SetupWebContents(content::WebContents* contents) {
}

void KeyboardUIContent::OnWindowBoundsChanged(aura::Window* window,
                                              const gfx::Rect& old_bounds,
                                              const gfx::Rect& new_bounds) {
  if (!shadow_) {
    shadow_.reset(new wm::Shadow());
    shadow_->Init(wm::Shadow::STYLE_ACTIVE);
    shadow_->layer()->SetVisible(true);
    DCHECK(keyboard_contents_->GetNativeView()->parent());
    keyboard_contents_->GetNativeView()->parent()->layer()->Add(
        shadow_->layer());
  }

  shadow_->SetContentBounds(new_bounds);
}

void KeyboardUIContent::OnWindowDestroyed(aura::Window* window) {
  window->RemoveObserver(this);
}

const aura::Window* KeyboardUIContent::GetKeyboardRootWindow() const {
  if (!keyboard_contents_) {
    return nullptr;
  }
  return keyboard_contents_->GetNativeView()->GetRootWindow();
}

void KeyboardUIContent::LoadContents(const GURL& url) {
  if (keyboard_contents_) {
    content::OpenURLParams params(url, content::Referrer(),
                                  WindowOpenDisposition::SINGLETON_TAB,
                                  ui::PAGE_TRANSITION_AUTO_TOPLEVEL, false);
    keyboard_contents_->OpenURL(params);
  }
}

const GURL& KeyboardUIContent::GetVirtualKeyboardUrl() {
  if (keyboard::IsInputViewEnabled()) {
    const GURL& override_url = GetOverrideContentUrl();
    return override_url.is_valid() ? override_url : default_url_;
  } else {
    return default_url_;
  }
}

bool KeyboardUIContent::ShouldEnableInsets(aura::Window* window) {
  aura::Window* keyboard_window = GetKeyboardWindow();
  return (keyboard_window->GetRootWindow() == window->GetRootWindow() &&
          keyboard::IsKeyboardOverscrollEnabled() &&
          keyboard_window->IsVisible() &&
          keyboard_controller()->keyboard_visible());
}

void KeyboardUIContent::AddBoundsChangedObserver(aura::Window* window) {
  aura::Window* target_window = window ? window->GetToplevelWindow() : nullptr;
  if (target_window)
    window_bounds_observer_->AddObservedWindow(target_window);
}

}  // namespace keyboard
