// Copyright (c) 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 "ui/views/corewm/tooltip_controller.h"

#include <stddef.h>

#include <utility>
#include <vector>

#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "ui/aura/client/capture_client.h"
#include "ui/aura/client/cursor_client.h"
#include "ui/aura/client/drag_drop_client.h"
#include "ui/aura/client/screen_position_client.h"
#include "ui/aura/env.h"
#include "ui/aura/window.h"
#include "ui/display/screen.h"
#include "ui/events/event.h"
#include "ui/gfx/font.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/text_elider.h"
#include "ui/views/corewm/tooltip.h"
#include "ui/views/widget/tooltip_manager.h"
#include "ui/wm/public/tooltip_client.h"

namespace views {
namespace corewm {
namespace {

const int kDelayForTooltipUpdateInMs = 500;
const int kDefaultTooltipShownTimeoutMs = 10000;
#if defined(OS_WIN)
// Drawing a long word in tooltip is very slow on Windows. crbug.com/513693
const size_t kMaxTooltipLength = 1024;
#else
const size_t kMaxTooltipLength = 2048;
#endif

// Returns true if |target| is a valid window to get the tooltip from.
// |event_target| is the original target from the event and |target| the window
// at the same location.
bool IsValidTarget(aura::Window* event_target, aura::Window* target) {
  if (!target || (event_target == target))
    return true;

  void* event_target_grouping_id = event_target->GetNativeWindowProperty(
      TooltipManager::kGroupingPropertyKey);
  void* target_grouping_id = target->GetNativeWindowProperty(
      TooltipManager::kGroupingPropertyKey);
  return event_target_grouping_id &&
      event_target_grouping_id == target_grouping_id;
}

// Returns the target (the Window tooltip text comes from) based on the event.
// If a Window other than event.target() is returned, |location| is adjusted
// to be in the coordinates of the returned Window.
aura::Window* GetTooltipTarget(const ui::MouseEvent& event,
                               gfx::Point* location) {
  switch (event.type()) {
    case ui::ET_MOUSE_CAPTURE_CHANGED:
      // On windows we can get a capture changed without an exit. We need to
      // reset state when this happens else the tooltip may incorrectly show.
      return NULL;
    case ui::ET_MOUSE_EXITED:
      return NULL;
    case ui::ET_MOUSE_MOVED:
    case ui::ET_MOUSE_DRAGGED: {
      aura::Window* event_target = static_cast<aura::Window*>(event.target());
      if (!event_target)
        return NULL;

      // If a window other than |event_target| has capture, ignore the event.
      // This can happen when RootWindow creates events when showing/hiding, or
      // the system generates an extra event. We have to check
      // GetGlobalCaptureWindow() as Windows does not use a singleton
      // CaptureClient.
      if (!event_target->HasCapture()) {
        aura::Window* root = event_target->GetRootWindow();
        if (root) {
          aura::client::CaptureClient* capture_client =
              aura::client::GetCaptureClient(root);
          if (capture_client) {
            aura::Window* capture_window =
                capture_client->GetGlobalCaptureWindow();
            if (capture_window && event_target != capture_window)
              return NULL;
          }
        }
        return event_target;
      }

      // If |target| has capture all events go to it, even if the mouse is
      // really over another window. Find the real window the mouse is over.
      gfx::Point screen_loc(event.location());
      aura::client::GetScreenPositionClient(event_target->GetRootWindow())->
          ConvertPointToScreen(event_target, &screen_loc);
      display::Screen* screen = display::Screen::GetScreen();
      aura::Window* target = screen->GetWindowAtScreenPoint(screen_loc);
      if (!target)
        return NULL;
      gfx::Point target_loc(screen_loc);
      aura::client::GetScreenPositionClient(target->GetRootWindow())->
          ConvertPointFromScreen(target, &target_loc);
      aura::Window* screen_target = target->GetEventHandlerForPoint(target_loc);
      if (!IsValidTarget(event_target, screen_target))
        return NULL;

      aura::Window::ConvertPointToTarget(screen_target, target, &target_loc);
      *location = target_loc;
      return screen_target;
    }
    default:
      NOTREACHED();
      break;
  }
  return NULL;
}

}  // namespace

////////////////////////////////////////////////////////////////////////////////
// TooltipController public:

TooltipController::TooltipController(std::unique_ptr<Tooltip> tooltip)
    : tooltip_window_(NULL),
      tooltip_id_(NULL),
      tooltip_window_at_mouse_press_(NULL),
      tooltip_(std::move(tooltip)),
      tooltips_enabled_(true),
      tooltip_show_delayed_(true) {}

TooltipController::~TooltipController() {
  if (tooltip_window_)
    tooltip_window_->RemoveObserver(this);
}

int TooltipController::GetMaxWidth(const gfx::Point& location) const {
  return tooltip_->GetMaxWidth(location);
}

void TooltipController::UpdateTooltip(aura::Window* target) {
  // If tooltip is visible, we may want to hide it. If it is not, we are ok.
  if (tooltip_window_ == target && tooltip_->IsVisible())
    UpdateIfRequired();

  // Reset |tooltip_window_at_mouse_press_| if the moving within the same window
  // but over a region that has different tooltip text.
  // This handles the case of clicking on a view, moving within the same window
  // but over a different view, than back to the original.
  if (tooltip_window_at_mouse_press_ &&
      target == tooltip_window_at_mouse_press_ &&
      wm::GetTooltipText(target) != tooltip_text_at_mouse_press_) {
    tooltip_window_at_mouse_press_ = NULL;
  }
}

void TooltipController::SetTooltipShownTimeout(aura::Window* target,
                                               int timeout_in_ms) {
  tooltip_shown_timeout_map_[target] = timeout_in_ms;
}

void TooltipController::SetTooltipsEnabled(bool enable) {
  if (tooltips_enabled_ == enable)
    return;
  tooltips_enabled_ = enable;
  UpdateTooltip(tooltip_window_);
}

void TooltipController::OnKeyEvent(ui::KeyEvent* event) {
  // On key press, we want to hide the tooltip and not show it until change.
  // This is the same behavior as hiding tooltips on timeout. Hence, we can
  // simply simulate a timeout.
  if (tooltip_shown_timer_.IsRunning()) {
    tooltip_shown_timer_.Stop();
    TooltipShownTimerFired();
  }
}

void TooltipController::OnMouseEvent(ui::MouseEvent* event) {
  // Ignore mouse events that coincide with the last touch event.
  if (event->location() == last_touch_loc_) {
    SetTooltipWindow(nullptr);

    if (tooltip_->IsVisible())
      UpdateIfRequired();
    return;
  }
  switch (event->type()) {
    case ui::ET_MOUSE_CAPTURE_CHANGED:
    case ui::ET_MOUSE_EXITED:
    case ui::ET_MOUSE_MOVED:
    case ui::ET_MOUSE_DRAGGED: {
      curr_mouse_loc_ = event->location();
      aura::Window* target = NULL;
      // Avoid a call to display::Screen::GetWindowAtScreenPoint() since it can
      // be very expensive on X11 in cases when the tooltip is hidden anyway.
      if (tooltips_enabled_ &&
          !aura::Env::GetInstance()->IsMouseButtonDown() &&
          !IsDragDropInProgress()) {
        target = GetTooltipTarget(*event, &curr_mouse_loc_);
      }
      SetTooltipWindow(target);

      if (tooltip_->IsVisible() ||
          (tooltip_window_ &&
           tooltip_text_ != wm::GetTooltipText(tooltip_window_)))
        UpdateIfRequired();
      break;
    }
    case ui::ET_MOUSE_PRESSED:
      if ((event->flags() & ui::EF_IS_NON_CLIENT) == 0) {
        aura::Window* target = static_cast<aura::Window*>(event->target());
        // We don't get a release for non-client areas.
        tooltip_window_at_mouse_press_ = target;
        if (target)
          tooltip_text_at_mouse_press_ = wm::GetTooltipText(target);
      }
      tooltip_->Hide();
      break;
    case ui::ET_MOUSEWHEEL:
      // Hide the tooltip for click, release, drag, wheel events.
      if (tooltip_->IsVisible())
        tooltip_->Hide();
      break;
    default:
      break;
  }
}

void TooltipController::OnTouchEvent(ui::TouchEvent* event) {
  // Hide the tooltip for touch events.
  tooltip_->Hide();
  SetTooltipWindow(NULL);
  last_touch_loc_ = event->location();
}

void TooltipController::OnCancelMode(ui::CancelModeEvent* event) {
  tooltip_->Hide();
  SetTooltipWindow(NULL);
}

void TooltipController::OnCursorVisibilityChanged(bool is_visible) {
  UpdateIfRequired();
}

void TooltipController::OnWindowDestroyed(aura::Window* window) {
  if (tooltip_window_ == window) {
    tooltip_->Hide();
    tooltip_shown_timeout_map_.erase(tooltip_window_);
    tooltip_window_ = NULL;
  }
}

void TooltipController::OnWindowPropertyChanged(aura::Window* window,
                                                const void* key,
                                                intptr_t old) {
  if ((key == wm::kTooltipIdKey || key == wm::kTooltipTextKey) &&
      wm::GetTooltipText(window) != base::string16() &&
      (tooltip_text_ != wm::GetTooltipText(window) ||
       tooltip_id_ != wm::GetTooltipId(window)))
    UpdateIfRequired();
}

////////////////////////////////////////////////////////////////////////////////
// TooltipController private:

void TooltipController::TooltipShownTimerFired() {
  tooltip_->Hide();
}

void TooltipController::UpdateIfRequired() {
  if (!tooltips_enabled_ || aura::Env::GetInstance()->IsMouseButtonDown() ||
      IsDragDropInProgress() || !IsCursorVisible()) {
    tooltip_->Hide();
    return;
  }

  base::string16 tooltip_text;
  if (tooltip_window_)
    tooltip_text = wm::GetTooltipText(tooltip_window_);

  // If the user pressed a mouse button. We will hide the tooltip and not show
  // it until there is a change in the tooltip.
  if (tooltip_window_at_mouse_press_) {
    if (tooltip_window_ == tooltip_window_at_mouse_press_ &&
        tooltip_text == tooltip_text_at_mouse_press_) {
      tooltip_->Hide();
      return;
    }
    tooltip_window_at_mouse_press_ = NULL;
  }

  // If the uniqueness indicator is different from the previously encountered
  // one, we should force tooltip update
  const void* tooltip_id = wm::GetTooltipId(tooltip_window_);
  bool ids_differ = false;
  ids_differ = tooltip_id_ != tooltip_id;
  tooltip_id_ = tooltip_id;

  // If we come here from UpdateTooltip(), we have already checked for tooltip
  // visibility and this check below will have no effect.
  if (tooltip_text_ != tooltip_text || !tooltip_->IsVisible() || ids_differ) {
    tooltip_shown_timer_.Stop();
    tooltip_text_ = tooltip_text;
    base::string16 trimmed_text =
        gfx::TruncateString(tooltip_text_, kMaxTooltipLength, gfx::WORD_BREAK);
    // If the string consists entirely of whitespace, then don't both showing it
    // (an empty tooltip is useless).
    base::TrimWhitespace(trimmed_text, base::TRIM_ALL,
                         &tooltip_text_whitespace_trimmed_);
    if (tooltip_text_whitespace_trimmed_.empty()) {
      tooltip_->Hide();
      tooltip_defer_timer_.Stop();
    } else if (tooltip_show_delayed_) {
      // Initialize the one-shot timer to show the tooltip in a while.
      // If there is already a request queued then cancel it and post the new
      // request. This ensures that tooltip won't show up too early.
      // The delayed appearance of a tooltip is by default.
      if (tooltip_defer_timer_.IsRunning()) {
        tooltip_defer_timer_.Reset();
      } else {
        tooltip_defer_timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds(
                                                  kDelayForTooltipUpdateInMs),
                                   this, &TooltipController::ShowTooltip);
      }
    } else {
      ShowTooltip();  // Allow tooltip to show up without delay for unit tests.
    }
  }
}

void TooltipController::ShowTooltip() {
  if (!tooltip_window_)
    return;
  gfx::Point widget_loc =
      curr_mouse_loc_ + tooltip_window_->GetBoundsInScreen().OffsetFromOrigin();
  tooltip_->SetText(tooltip_window_, tooltip_text_whitespace_trimmed_,
                    widget_loc);
  tooltip_->Show();
  int timeout = GetTooltipShownTimeout();
  if (timeout > 0) {
    tooltip_shown_timer_.Start(FROM_HERE,
                               base::TimeDelta::FromMilliseconds(timeout), this,
                               &TooltipController::TooltipShownTimerFired);
  }
}

bool TooltipController::IsTooltipVisible() {
  return tooltip_->IsVisible();
}

bool TooltipController::IsDragDropInProgress() {
  if (!tooltip_window_)
    return false;
  aura::client::DragDropClient* client =
      aura::client::GetDragDropClient(tooltip_window_->GetRootWindow());
  return client && client->IsDragDropInProgress();
}

bool TooltipController::IsCursorVisible() {
  if (!tooltip_window_)
    return false;
  aura::Window* root = tooltip_window_->GetRootWindow();
  if (!root)
    return false;
  aura::client::CursorClient* cursor_client =
      aura::client::GetCursorClient(root);
  // |cursor_client| may be NULL in tests, treat NULL as always visible.
  return !cursor_client || cursor_client->IsCursorVisible();
}

int TooltipController::GetTooltipShownTimeout() {
  std::map<aura::Window*, int>::const_iterator it =
      tooltip_shown_timeout_map_.find(tooltip_window_);
  if (it == tooltip_shown_timeout_map_.end())
    return kDefaultTooltipShownTimeoutMs;
  return it->second;
}

void TooltipController::SetTooltipWindow(aura::Window* target) {
  if (tooltip_window_ == target)
    return;
  if (tooltip_window_)
    tooltip_window_->RemoveObserver(this);
  tooltip_window_ = target;
  if (tooltip_window_)
    tooltip_window_->AddObserver(this);
}

}  // namespace corewm
}  // namespace views
