// 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/widget/tooltip_manager_win.h"

#include <windowsx.h>

#include <limits>
#include <vector>

#include "base/bind.h"
#include "base/i18n/rtl.h"
#include "base/logging.h"
#include "base/message_loop/message_loop.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/win/scoped_hdc.h"
#include "base/win/scoped_select_object.h"
#include "ui/base/l10n/l10n_util_win.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/screen.h"
#include "ui/gfx/text_elider.h"
#include "ui/gfx/text_utils.h"
#include "ui/gfx/win/dpi.h"
#include "ui/gfx/win/hwnd_util.h"
#include "ui/gfx/win/scoped_set_map_mode.h"
#include "ui/views/view.h"
#include "ui/views/widget/monitor_win.h"
#include "ui/views/widget/widget.h"

namespace views {

namespace {

static int tooltip_height_ = 0;

// Maximum number of lines we allow in the tooltip.
const size_t kMaxLines = 6;

// Trims the tooltip to fit, setting |text| to the clipped result, |max_width|
// to the width (in pixels) of the clipped text and |line_count| to the number
// of lines of text in the tooltip. |available_width| gives the space available
// for the tooltip.
void TrimTooltipToFit(const gfx::FontList& font_list,
                      int available_width,
                      base::string16* text,
                      int* max_width,
                      int* line_count) {
  *max_width = 0;
  *line_count = 0;

  TooltipManager::TrimTooltipText(text);

  // Split the string into at most kMaxLines lines.
  std::vector<base::string16> lines;
  base::SplitString(*text, '\n', &lines);
  if (lines.size() > kMaxLines)
    lines.resize(kMaxLines);
  *line_count = static_cast<int>(lines.size());

  // Format each line to fit.
  base::string16 result;
  for (std::vector<base::string16>::iterator i = lines.begin();
       i != lines.end(); ++i) {
    base::string16 elided_text =
        gfx::ElideText(*i, font_list, available_width, gfx::ELIDE_AT_END);
    *max_width = std::max(*max_width,
                          gfx::GetStringWidth(elided_text, font_list));
    if (!result.empty())
      result.push_back('\n');
    result.append(elided_text);
  }
  *text = result;
}

}  // namespace

// static
int TooltipManager::GetTooltipHeight() {
  DCHECK_GT(tooltip_height_, 0);
  return tooltip_height_;
}

static gfx::Font DetermineDefaultFont() {
  HWND window = CreateWindowEx(
      WS_EX_TRANSPARENT | l10n_util::GetExtendedTooltipStyles(),
      TOOLTIPS_CLASS, NULL, 0 , 0, 0, 0, 0, NULL, NULL, NULL, NULL);
  if (!window)
    return gfx::Font();
  HFONT hfont = reinterpret_cast<HFONT>(SendMessage(window, WM_GETFONT, 0, 0));
  gfx::Font font = hfont ? gfx::Font(hfont) : gfx::Font();
  DestroyWindow(window);
  return font;
}

TooltipManagerWin::TooltipManagerWin(Widget* widget)
    : widget_(widget),
      tooltip_hwnd_(NULL),
      last_mouse_pos_(-1, -1),
      tooltip_showing_(false),
      last_tooltip_view_(NULL),
      last_view_out_of_sync_(false),
      tooltip_width_(0) {
  DCHECK(widget);
  DCHECK(widget->GetNativeView());
}

TooltipManagerWin::~TooltipManagerWin() {
  if (tooltip_hwnd_)
    DestroyWindow(tooltip_hwnd_);
}

bool TooltipManagerWin::Init() {
  DCHECK(!tooltip_hwnd_);
  // Create the tooltip control.
  tooltip_hwnd_ = CreateWindowEx(
      WS_EX_TRANSPARENT | l10n_util::GetExtendedTooltipStyles(),
      TOOLTIPS_CLASS, NULL, TTS_NOPREFIX, 0, 0, 0, 0,
      GetParent(), NULL, NULL, NULL);
  if (!tooltip_hwnd_)
    return false;

  l10n_util::AdjustUIFontForWindow(tooltip_hwnd_);

  // This effectively turns off clipping of tooltips. We need this otherwise
  // multi-line text (\r\n) won't work right. The size doesn't really matter
  // (just as long as its bigger than the monitor's width) as we clip to the
  // screen size before rendering.
  SendMessage(tooltip_hwnd_, TTM_SETMAXTIPWIDTH, 0,
              std::numeric_limits<int16>::max());

  // Add one tool that is used for all tooltips.
  toolinfo_.cbSize = sizeof(toolinfo_);
  toolinfo_.uFlags = TTF_TRANSPARENT | TTF_IDISHWND;
  toolinfo_.hwnd = GetParent();
  toolinfo_.uId = reinterpret_cast<UINT_PTR>(GetParent());
  // Setting this tells windows to call GetParent() back (using a WM_NOTIFY
  // message) for the actual tooltip contents.
  toolinfo_.lpszText = LPSTR_TEXTCALLBACK;
  toolinfo_.lpReserved = NULL;
  SetRectEmpty(&toolinfo_.rect);
  SendMessage(tooltip_hwnd_, TTM_ADDTOOL, 0, (LPARAM)&toolinfo_);
  return true;
}

gfx::NativeView TooltipManagerWin::GetParent() {
  return widget_->GetNativeView();
}

const gfx::FontList& TooltipManagerWin::GetFontList() const {
  static gfx::FontList* font_list = NULL;
  if (!font_list)
    font_list = new gfx::FontList(DetermineDefaultFont());
  return *font_list;
}

void TooltipManagerWin::UpdateTooltip() {
  // Set last_view_out_of_sync_ to indicate the view is currently out of sync.
  // This doesn't update the view under the mouse immediately as it may cause
  // timing problems.
  last_view_out_of_sync_ = true;
  last_tooltip_view_ = NULL;
  // Hide the tooltip.
  SendMessage(tooltip_hwnd_, TTM_POP, 0, 0);
}

void TooltipManagerWin::TooltipTextChanged(View* view) {
  if (view == last_tooltip_view_)
    UpdateTooltip(last_mouse_pos_);
}

LRESULT TooltipManagerWin::OnNotify(int w_param,
                                    NMHDR* l_param,
                                    bool* handled) {
  *handled = false;
  if (l_param->hwndFrom != tooltip_hwnd_)
    return 0;

  switch (l_param->code) {
    case TTN_GETDISPINFO: {
      if (last_view_out_of_sync_) {
        // View under the mouse is out of sync, determine it now.
        View* root_view = widget_->GetRootView();
        last_tooltip_view_ =
            root_view->GetTooltipHandlerForPoint(last_mouse_pos_);
        last_view_out_of_sync_ = false;
      }
      // Tooltip control is asking for the tooltip to display.
      NMTTDISPINFOW* tooltip_info =
          reinterpret_cast<NMTTDISPINFOW*>(l_param);
      // Initialize the string, if we have a valid tooltip the string will
      // get reset below.
      tooltip_info->szText[0] = TEXT('\0');
      tooltip_text_.clear();
      tooltip_info->lpszText = NULL;
      clipped_text_.clear();
      if (last_tooltip_view_ != NULL) {
        tooltip_text_.clear();
        // Mouse is over a View, ask the View for its tooltip.
        gfx::Point view_loc = last_mouse_pos_;
        View::ConvertPointToTarget(widget_->GetRootView(),
                                   last_tooltip_view_, &view_loc);
        if (last_tooltip_view_->GetTooltipText(view_loc, &tooltip_text_) &&
            !tooltip_text_.empty()) {
          // View has a valid tip, copy it into TOOLTIPINFO.
          clipped_text_ = tooltip_text_;
          gfx::Point screen_loc = last_mouse_pos_;
          View::ConvertPointToScreen(widget_->GetRootView(), &screen_loc);
          TrimTooltipToFit(
              GetFontList(),
              GetMaxWidth(screen_loc.x(), screen_loc.y(),
                          widget_->GetNativeView()),
              &clipped_text_, &tooltip_width_, &line_count_);
          // Adjust the clipped tooltip text for locale direction.
          base::i18n::AdjustStringForLocaleDirection(&clipped_text_);
          tooltip_info->lpszText = const_cast<WCHAR*>(clipped_text_.c_str());
        } else {
          tooltip_text_.clear();
        }
      }
      *handled = true;
      return 0;
    }
    case TTN_POP:
      tooltip_showing_ = false;
      *handled = true;
      return 0;
    case TTN_SHOW: {
      *handled = true;
      tooltip_showing_ = true;
      // The tooltip is about to show, allow the view to position it
      gfx::Point text_origin;
      if (tooltip_height_ == 0)
        tooltip_height_ = CalcTooltipHeight();
      gfx::Point view_loc = last_mouse_pos_;
      View::ConvertPointToTarget(widget_->GetRootView(),
                                 last_tooltip_view_, &view_loc);
      if (last_tooltip_view_->GetTooltipTextOrigin(view_loc, &text_origin) &&
          SetTooltipPosition(text_origin.x(), text_origin.y())) {
        // Return true, otherwise the rectangle we specified is ignored.
        return TRUE;
      }
      return 0;
    }
    default:
      // Fall through.
      break;
  }
  return 0;
}

bool TooltipManagerWin::SetTooltipPosition(int text_x, int text_y) {
  // NOTE: this really only tests that the y location fits on screen, but that
  // is good enough for our usage.

  // Calculate the bounds the tooltip will get.
  gfx::Point view_loc;
  View::ConvertPointToScreen(last_tooltip_view_, &view_loc);
  view_loc = gfx::win::DIPToScreenPoint(view_loc);
  RECT bounds = { view_loc.x() + text_x,
                  view_loc.y() + text_y,
                  view_loc.x() + text_x + tooltip_width_,
                  view_loc.y() + line_count_ * GetTooltipHeight() };
  SendMessage(tooltip_hwnd_, TTM_ADJUSTRECT, TRUE, (LPARAM)&bounds);

  // Make sure the rectangle completely fits on the current monitor. If it
  // doesn't, return false so that windows positions the tooltip at the
  // default location.
  gfx::Rect monitor_bounds =
      views::GetMonitorBoundsForRect(gfx::Rect(bounds.left, bounds.right,
                                                  0, 0));
  if (!monitor_bounds.Contains(gfx::Rect(bounds))) {
    return false;
  }

  ::SetWindowPos(tooltip_hwnd_, NULL, bounds.left, bounds.top, 0, 0,
                 SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOSIZE);
  return true;
}

int TooltipManagerWin::CalcTooltipHeight() {
  // Ask the tooltip for its font.
  int height;
  HFONT hfont = reinterpret_cast<HFONT>(
      SendMessage(tooltip_hwnd_, WM_GETFONT, 0, 0));
  if (hfont != NULL) {
    base::win::ScopedGetDC dc(tooltip_hwnd_);
    base::win::ScopedSelectObject font(dc, hfont);
    gfx::ScopedSetMapMode mode(dc, MM_TEXT);
    TEXTMETRIC font_metrics;
    GetTextMetrics(dc, &font_metrics);
    height = font_metrics.tmHeight;
  } else {
    // Tooltip is using the system font. Use gfx::Font, which should pick
    // up the system font.
    height = gfx::Font().GetHeight();
  }
  // Get the margins from the tooltip
  RECT tooltip_margin;
  SendMessage(tooltip_hwnd_, TTM_GETMARGIN, 0, (LPARAM)&tooltip_margin);
  return height + tooltip_margin.top + tooltip_margin.bottom;
}

void TooltipManagerWin::UpdateTooltip(const gfx::Point& mouse_pos) {
  View* root_view = widget_->GetRootView();
  View* view = root_view->GetTooltipHandlerForPoint(mouse_pos);
  if (view != last_tooltip_view_) {
    // NOTE: This *must* be sent regardless of the visibility of the tooltip.
    // It triggers Windows to ask for the tooltip again.
    SendMessage(tooltip_hwnd_, TTM_POP, 0, 0);
    last_tooltip_view_ = view;
  } else if (last_tooltip_view_ != NULL) {
    // Tooltip is showing, and mouse is over the same view. See if the tooltip
    // text has changed.
    gfx::Point view_point = mouse_pos;
    View::ConvertPointToTarget(root_view, last_tooltip_view_, &view_point);
    string16 new_tooltip_text;
    bool has_tooltip_text =
        last_tooltip_view_->GetTooltipText(view_point, &new_tooltip_text);
    if (!has_tooltip_text || (new_tooltip_text != tooltip_text_)) {
      // The text has changed, hide the popup.
      SendMessage(tooltip_hwnd_, TTM_POP, 0, 0);
      if (has_tooltip_text && !new_tooltip_text.empty() && tooltip_showing_) {
        // New text is valid, show the popup.
        SendMessage(tooltip_hwnd_, TTM_POPUP, 0, 0);
      }
    }
  }
}

void TooltipManagerWin::OnMouse(UINT u_msg, WPARAM w_param, LPARAM l_param) {
  gfx::Point mouse_pos_in_pixels(l_param);
  gfx::Point mouse_pos = gfx::win::ScreenToDIPPoint(mouse_pos_in_pixels);

  if (u_msg >= WM_NCMOUSEMOVE && u_msg <= WM_NCXBUTTONDBLCLK) {
    // NC message coordinates are in screen coordinates.
    POINT temp = mouse_pos_in_pixels.ToPOINT();
    ::MapWindowPoints(HWND_DESKTOP, GetParent(), &temp, 1);
    mouse_pos_in_pixels.SetPoint(temp.x, temp.y);
    mouse_pos = gfx::win::ScreenToDIPPoint(mouse_pos_in_pixels);
  }

  if (u_msg != WM_MOUSEMOVE || last_mouse_pos_ != mouse_pos) {
    last_mouse_pos_ = mouse_pos;
    UpdateTooltip(mouse_pos);
  }
  // Forward the message onto the tooltip.
  MSG msg;
  msg.hwnd = GetParent();
  msg.message = u_msg;
  msg.wParam = w_param;
  msg.lParam = l_param;
  SendMessage(tooltip_hwnd_, TTM_RELAYEVENT, 0, (LPARAM)&msg);
}

}  // namespace views