// Copyright 2018 The Chromium Authors
// 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/omnibox/omnibox_text_view.h"

#include <limits.h>

#include <algorithm>
#include <memory>
#include <optional>
#include <string_view>

#include "base/memory/safety_checks.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/omnibox/omnibox_theme.h"
#include "chrome/browser/ui/views/chrome_typography.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/color/color_provider.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/range/range.h"
#include "ui/gfx/render_text.h"
#include "ui/views/style/typography.h"
#include "ui/views/style/typography_provider.h"

namespace {

// Use the primary style for everything. TextStyle sometimes controls color, but
// we use OmniboxTheme for that.
constexpr int kTextStyle = views::style::STYLE_PRIMARY;

// The vertical padding to provide each RenderText in addition to the height
// of the font. Where possible, RenderText uses this additional space to
// vertically center the cap height of the font instead of centering the
// entire font.
static constexpr int kVerticalPadding = 3;

void ApplyTextStyleForAnswer(OmniboxResultView* result_view,
                             gfx::RenderText* render_text,
                             const gfx::Range& range,
                             bool is_headline) {
  render_text->ApplyWeight(gfx::Font::Weight::NORMAL, range);
  render_text->ApplyBaselineStyle(gfx::BaselineStyle::kNormalBaseline, range);
  const bool selected =
      result_view->GetThemeState() == OmniboxPartState::SELECTED;
  ui::ColorId id;
  if (is_headline) {
    id = selected ? kColorOmniboxResultsTextSelected : kColorOmniboxText;
  } else {
    id = selected ? kColorOmniboxResultsTextDimmedSelected
                  : kColorOmniboxResultsTextDimmed;
  }
  render_text->ApplyColor(result_view->GetColorProvider()->GetColor(id), range);
}

// Dictionary and translation answers have a max number of lines > 1.
bool AnswerHasDefinedMaxLines(omnibox::AnswerType answer_type) {
  return answer_type == omnibox::ANSWER_TYPE_DICTIONARY ||
         answer_type == omnibox::ANSWER_TYPE_TRANSLATION;
}

}  // namespace

OmniboxTextView::OmniboxTextView(OmniboxResultView* result_view)
    : result_view_(result_view) {
  SetCanProcessEventsWithinSubtree(false);
}

OmniboxTextView::~OmniboxTextView() = default;

gfx::Size OmniboxTextView::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  if (!render_text_) {
    return gfx::Size();
  }

  if (!available_size.width().is_bounded()) {
    render_text_->SetDisplayRect(gfx::Rect(gfx::Size(INT_MAX, 0)));
    return render_text_->GetStringSize();
  }

  const int width = available_size.width().value();
  if (!wrap_text_lines_) {
    return gfx::Size(width, GetLineHeight());
  }

  if (cached_available_size_ != available_size ||
      cached_calculate_preferred_size_.IsEmpty()) {
    cached_available_size_ = available_size;
    render_text_->SetDisplayRect(gfx::Rect(width, 0));
    gfx::Size string_size = render_text_->GetStringSize();
    string_size.Enlarge(0, kVerticalPadding);
    cached_calculate_preferred_size_ = string_size;
  }
  return cached_calculate_preferred_size_;
}

void OmniboxTextView::OnPaint(gfx::Canvas* canvas) {
  // Omnibox interaction is a critical user journey we exclude it from
  // additional memory safety checks.
  // TODO(crbug.com/478634529): Optimize and remove if possible.
  base::ScopedSafetyChecksExclusion excluded;
  View::OnPaint(canvas);

  if (!render_text_) {
    return;
  }
  render_text_->SetDisplayRect(GetContentsBounds());
  render_text_->Draw(canvas);
}

void OmniboxTextView::ApplyTextColor(ui::ColorId id) {
  if (GetText().empty()) {
    return;
  }
  render_text_->SetColor(GetColorProvider()->GetColor(id));
  SchedulePaint();
}

std::u16string_view OmniboxTextView::GetText() const {
  return render_text_ ? render_text_->text() : std::u16string_view();
}

void OmniboxTextView::SetText(std::u16string_view new_text) {
  if (cached_classifications_) {
    cached_classifications_.reset();
  } else if (GetText() == new_text) {
    // Only exit early if |cached_classifications_| was empty,
    // i.e. the last time text was set was through this method.
    return;
  }

  render_text_ = CreateRenderText(new_text);

  OnStyleChanged();
}

void OmniboxTextView::SetTextWithStyling(
    std::u16string_view new_text,
    const ACMatchClassifications& classifications) {
  if (GetText() == new_text && cached_classifications_ &&
      classifications == *cached_classifications_) {
    return;
  }

  cached_classifications_ =
      std::make_unique<ACMatchClassifications>(classifications);
  render_text_ = CreateRenderText(new_text);

  // `ReapplyStyling()` will update the preferred size and request a repaint.
  ReapplyStyling();
}

void OmniboxTextView::AppendAndStyleAnswerText(
    const omnibox::FormattedString& formatted_string,
    size_t fragment_index,
    const omnibox::AnswerType& answer_type,
    bool is_headline) {
  cached_classifications_.reset();
  wrap_text_lines_ = AnswerHasDefinedMaxLines(answer_type);
  const auto fragments_size =
      static_cast<size_t>(formatted_string.fragments_size());
  for (size_t i = fragment_index; i < fragments_size; ++i) {
    const std::u16string space_separator = i == 0u ? u"" : u" ";
    const std::u16string append_text =
        space_separator +
        base::UTF8ToUTF16(formatted_string.fragments(i).text());
    size_t offset = render_text_ ? render_text_->text().length() : 0u;
    gfx::Range range(offset, offset + append_text.length());
    render_text_->AppendText(append_text);
    ApplyTextStyleForAnswer(result_view_, render_text_.get(), range,
                            is_headline);
  }
  OnStyleChanged();
}

void OmniboxTextView::SetMultilineAnswerText(
    const omnibox::FormattedString& formatted_string,
    const omnibox::AnswerType& answer_type) {
  render_text_ = CreateRenderText(u"");
  if (formatted_string.fragments_size() > 0 &&
      AnswerHasDefinedMaxLines(answer_type)) {
    render_text_->SetMultiline(true);
    render_text_->SetMaxLines(1);
  }
  AppendAndStyleAnswerText(formatted_string, /*fragment_index=*/0u, answer_type,
                           /*is_headline=*/false);
}

void OmniboxTextView::SetMultilineText(const std::u16string& text) {
  // This is a "dirty" check to check if this call is a no-op and no state
  // change is required. It works only because `SetMultilineText()` is the only
  // place to set max lines to 3. It's not 100% correct because some of the
  // other setters don't reset max lines back to 1. Ideally, all these setter
  // methods would have a clean and robust way to verify no state change is
  // required and early exit. See comment at `wrap_text_lines_`'s declaration.
  if (!cached_classifications_ && GetText() == text && render_text_ &&
      render_text_->max_lines() == 3) {
    return;
  }
  cached_classifications_.reset();
  render_text_ = CreateRenderText(text);
  render_text_->SetColor(result_view_->GetColorProvider()->GetColor(
      kColorOmniboxResultsTextAnswer));
  render_text_->SetMultiline(true);
  render_text_->SetMaxLines(3);
  wrap_text_lines_ = true;
  OnStyleChanged();
}

int OmniboxTextView::GetLineHeight() const {
  return font_height_;
}

void OmniboxTextView::ReapplyStyling() {
  // No work required if there are no preexisting styles.
  if (!cached_classifications_) {
    return;
  }

  const size_t text_length = GetText().length();
  for (size_t i = 0; i < cached_classifications_->size(); ++i) {
    const size_t text_start = (*cached_classifications_)[i].offset;
    if (text_start >= text_length) {
      break;
    }

    const size_t text_end =
        (i < (cached_classifications_->size() - 1))
            ? std::min((*cached_classifications_)[i + 1].offset, text_length)
            : text_length;
    const gfx::Range current_range(text_start, text_end);

    // Calculate style-related data.
    if ((*cached_classifications_)[i].style & ACMatchClassification::MATCH) {
      render_text_->ApplyWeight(gfx::Font::Weight::BOLD, current_range);
    } else if ((*cached_classifications_)[i].style &
               ACMatchClassification::TOOLBELT) {
      // TODO(crbug.com/430318151): Investigate whether this font weight and
      // size handling can be done in chrome_typography.cc, like other omnibox
      // text.
      render_text_->ApplyWeight(gfx::Font::Weight::MEDIUM, current_range);
      render_text_->ApplyFontSizeOverride(12, current_range);
    }

    const bool selected =
        result_view_->GetThemeState() == OmniboxPartState::SELECTED;
    ui::ColorId id =
        selected ? kColorOmniboxResultsTextSelected : kColorOmniboxText;
    if ((*cached_classifications_)[i].style & ACMatchClassification::URL) {
      id = selected ? kColorOmniboxResultsUrlSelected : kColorOmniboxResultsUrl;
      render_text_->SetDirectionalityMode(gfx::DIRECTIONALITY_AS_URL);
    } else if ((*cached_classifications_)[i].style &
               ACMatchClassification::DIM) {
      id = selected ? kColorOmniboxResultsTextDimmedSelected
                    : kColorOmniboxResultsTextDimmed;
    }
    render_text_->ApplyColor(GetColorProvider()->GetColor(id), current_range);
  }

  OnStyleChanged();
}

std::unique_ptr<gfx::RenderText> OmniboxTextView::CreateRenderText(
    std::u16string_view text) const {
  std::unique_ptr<gfx::RenderText> render_text =
      gfx::RenderText::CreateRenderText();
  render_text->SetDisplayRect(gfx::Rect(gfx::Size(INT_MAX, 0)));
  render_text->SetCursorEnabled(false);
  render_text->SetElideBehavior(gfx::ELIDE_TAIL);
  const gfx::FontList& font = views::TypographyProvider::Get().GetFont(
      CONTEXT_OMNIBOX_POPUP, kTextStyle);
  render_text->SetFontList(font);
  render_text->SetText(text);
  // Increase space between lines for multiline texts.
  render_text->SetMinLineHeight(19);
  return render_text;
}

void OmniboxTextView::OnStyleChanged() {
  const int height_normal = render_text_->font_list().GetHeight();
  const int size_delta =
      render_text_->font_list().GetFontSize() - gfx::FontList().GetFontSize();
  const int height_bold =
      ui::ResourceBundle::GetSharedInstance()
          .GetFontListForDetails(ui::ResourceBundle::FontDetails(
              std::string(), size_delta, gfx::Font::Weight::BOLD))
          .GetHeight();
  font_height_ = std::max(height_normal, height_bold);
  font_height_ += kVerticalPadding;

  // Cache preferred size for 1-line matches only since their heights don't
  // depend on the available width.
  if (!wrap_text_lines_) {
    SetPreferredSize(CalculatePreferredSize({}));
  } else {
    SetPreferredSize({});
    cached_calculate_preferred_size_.SetSize(0, 0);
  }
  SchedulePaint();
}

BEGIN_METADATA(OmniboxTextView)
ADD_PROPERTY_METADATA(std::u16string, Text)
ADD_READONLY_PROPERTY_METADATA(int, LineHeight)
END_METADATA
