// Copyright 2013 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/toolbar/reload_button.h"

#include <stddef.h>

#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/command_updater.h"
#include "chrome/browser/external_protocol/external_protocol_handler.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/view_ids.h"
#include "chrome/browser/ui/waap/initial_webui_window_metrics_manager.h"
#include "chrome/browser/ui/waap/waap_ui_metrics_recorder.h"
#include "chrome/grit/browser_resources.h"
#include "chrome/grit/generated_resources.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/mojom/menu_source_type.mojom-forward.h"
#include "ui/base/pointer/touch_ui_controller.h"
#include "ui/base/theme_provider.h"
#include "ui/base/ui_base_features.h"
#include "ui/base/window_open_disposition.h"
#include "ui/base/window_open_disposition_utils.h"
#include "ui/compositor/compositor.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/menus/simple_menu_model.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/metrics.h"
#include "ui/views/property_effects.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/widget/widget.h"

namespace {

static constexpr views::SingleAnimatedImageContainer::AnimationBoundary
    kReloadStopAnimationBoundary = {.start_offset = 0.0f,
                                    .end_offset = 123.0f / 400.0f};
static constexpr views::SingleAnimatedImageContainer::AnimationBoundary
    kStopReloadAnimationBoundary = {.start_offset = 200 / 400.0f,
                                    .end_offset = 300 / 400.0f};
static constexpr base::TimeDelta kIconAnimationTime = base::Milliseconds(300);
static constexpr gfx::Tween::Type kIconAnimationTween =
    gfx::Tween::Type::FAST_OUT_SLOW_IN_3;
}  // namespace

// ReloadButton ---------------------------------------------------------------

ReloadButton::ReloadButton(
    Profile* profile,
    CommandUpdater* command_updater,
    InitialWebUIWindowMetricsManager* window_metrics_manager)
    : ToolbarButton(base::BindRepeating(&ReloadButton::ButtonPressed,
                                        base::Unretained(this)),
                    CreateMenuModel(),
                    nullptr),
      metrics_recorder_(std::make_unique<WaapUIMetricsRecorder>(profile)),
      command_updater_(command_updater),
      reload_icon_(features::IsRoundedIconsEnabled()
                       ? vector_icons::kRefreshIcon
                       : vector_icons::kReloadChromeRefreshOldIcon),
      reload_touch_icon_(features::IsRoundedIconsEnabled()
                             ? vector_icons::kRefreshIcon
                             : kReloadTouchOldIcon),
      stop_icon_(features::IsRoundedIconsEnabled()
                     ? kCloseIcon
                     : kNavigateStopChromeRefreshOldIcon),
      stop_touch_icon_(features::IsRoundedIconsEnabled()
                           ? kCloseIcon
                           : kNavigateStopTouchOldIcon),
      double_click_timer_delay_(views::GetDoubleClickInterval()),
      mode_switch_timer_delay_(base::Milliseconds(1350)),
      window_metrics_manager_(window_metrics_manager) {
  if (window_metrics_manager_) {
    window_metrics_manager_->OnReloadButtonCreated();
  }
  SetVisibleMode(Mode::kReload);
  SetTriggerableEventFlags(ui::EF_LEFT_MOUSE_BUTTON |
                           ui::EF_MIDDLE_MOUSE_BUTTON);
  GetViewAccessibility().SetName(l10n_util::GetStringUTF16(IDS_ACCNAME_RELOAD));
  UpdateAccessibleHasPopup();
  SetProperty(views::kElementIdentifierKey, kReloadButtonElementId);
  SetID(VIEW_ID_RELOAD_BUTTON);
  UpdateCachedTooltipText();
}

ReloadButton::~ReloadButton() = default;

bool ReloadButton::GetDoubleClickTimerIsRunning() const {
  return double_click_timer_.IsRunning();
}

void ReloadButton::ChangeMode(Mode mode, bool force) {
  intended_mode_ = mode;

  // If the change is forced, or the user isn't hovering the icon, or it's
  // safe to change it to the other image type, make the change immediately;
  // otherwise we'll let it happen later.
  if (force || (!IsMouseHovered() && !testing_mouse_hovered_) ||
      ((mode == Mode::kStop) ? !double_click_timer_.IsRunning()
                             : (visible_mode_ != Mode::kStop))) {
    double_click_timer_.Stop();
    mode_switch_timer_.Stop();
    SetVisibleMode(mode);
    SetEnabled(true);

    // We want to disable the button if we're preventing a change from stop to
    // reload due to hovering, but not if we're preventing a change from
    // reload to stop due to the double-click timer running.  (Disabled reload
    // state is only applicable when instant extended API is enabled and mode
    // is NTP, which is handled just above.)
  } else if (visible_mode_ != Mode::kReload) {
    SetEnabled(false);

    // Go ahead and change to reload after a bit, which allows repeated
    // reloads without moving the mouse.
    if (!mode_switch_timer_.IsRunning()) {
      mode_switch_timer_.Start(FROM_HERE, mode_switch_timer_delay_, this,
                               &ReloadButton::OnStopToReloadTimer);
    }
  }
}

bool ReloadButton::GetDevToolsStatusForTesting() const {
  return is_dev_tools_connected_;
}

void ReloadButton::SetDevToolsStatus(bool is_dev_tools_connected) {
  is_dev_tools_connected_ = is_dev_tools_connected;
  UpdateAccessibleHasPopup();
  UpdateCachedTooltipText();
}

void ReloadButton::OnMouseExited(const ui::MouseEvent& event) {
  ToolbarButton::OnMouseExited(event);
  if (!IsMenuShowing()) {
    ChangeMode(intended_mode_, true);
  }
}

void ReloadButton::PaintButtonContents(gfx::Canvas* canvas) {
  // This has to be called every time the button is painted for various metrics.
  GetWidget()->GetCompositor()->RequestSuccessfulPresentationTimeForNextFrame(
      base::BindOnce(&ReloadButton::OnNextPresentation,
                     weak_ptr_factory_.GetWeakPtr()));
  Button::PaintButtonContents(canvas);
}

void ReloadButton::UpdateCachedTooltipText() {
  int reload_tooltip =
      ShouldShowMenu() ? IDS_TOOLTIP_RELOAD_WITH_MENU : IDS_TOOLTIP_RELOAD;
  SetTooltipText(l10n_util::GetStringUTF16(
      visible_mode_ == Mode::kReload ? reload_tooltip : IDS_TOOLTIP_STOP));
}

bool ReloadButton::ShouldShowMenu() {
  return is_dev_tools_connected_ && (visible_mode_ == Mode::kReload);
}

void ReloadButton::ShowDropDownMenu(ui::mojom::MenuSourceType source_type) {
  ToolbarButton::ShowDropDownMenu(source_type);  // Blocks.
  ChangeMode(intended_mode_, true);
}

bool ReloadButton::IsCommandIdChecked(int command_id) const {
  return false;
}

bool ReloadButton::IsCommandIdEnabled(int command_id) const {
  return true;
}

bool ReloadButton::IsCommandIdVisible(int command_id) const {
  return true;
}

bool ReloadButton::GetAcceleratorForCommandId(
    int command_id,
    ui::Accelerator* accelerator) const {
  return GetWidget()->GetAccelerator(command_id, accelerator);
}

void ReloadButton::ExecuteCommand(int command_id, int event_flags) {
  ExecuteBrowserCommand(command_id, event_flags);
}

std::unique_ptr<ui::SimpleMenuModel> ReloadButton::CreateMenuModel() {
  auto menu_model = std::make_unique<ui::SimpleMenuModel>(this);
  menu_model->AddItemWithStringId(IDC_RELOAD,
                                  IDS_RELOAD_MENU_NORMAL_RELOAD_ITEM);
  menu_model->AddItemWithStringId(IDC_RELOAD_BYPASSING_CACHE,
                                  IDS_RELOAD_MENU_HARD_RELOAD_ITEM);
  menu_model->AddItemWithStringId(IDC_RELOAD_CLEARING_CACHE,
                                  IDS_RELOAD_MENU_EMPTY_AND_HARD_RELOAD_ITEM);
  return menu_model;
}

void ReloadButton::SetVisibleMode(Mode mode) {
  const bool play_animation = features::IsToolbarGlowUpReloadEnabled() &&
                              !ui::TouchUiController::Get()->touch_ui() &&
                              animate_transitions_;

  switch (mode) {
    case Mode::kReload:
      if (play_animation) {
        std::optional<float> progress =
            animated_image_container().animation_progress();

        if (progress.has_value()) {
          if (visible_mode_ == Mode::kStop &&
              progress > kReloadStopAnimationBoundary.start_offset &&
              progress < kReloadStopAnimationBoundary.end_offset) {
            // We are interrupted to go to reload while animating reload to
            // stop. In this case we will play the rest of our current animation
            // and then immediately animate stop to reload, but with the
            // duration scaled down depending on how far along we are in the
            // reload to stop animation.

            static constexpr float kMinAnimationTimeScale = 0.66f;
            const float time_scale = gfx::Tween::FloatValueBetween(
                progress.value() / (kReloadStopAnimationBoundary.end_offset -
                                    kReloadStopAnimationBoundary.start_offset),
                kMinAnimationTimeScale, 1.0f);

            std::vector<views::SingleAnimatedImageContainer::AnimationConfig>
                configs;
            configs.push_back(
                {.boundary =
                     views::SingleAnimatedImageContainer::AnimationBoundary{
                         .start_offset = progress.value(),
                         .end_offset = kReloadStopAnimationBoundary.end_offset},
                 .tween = kIconAnimationTween,
                 .duration = time_scale * kIconAnimationTime});
            configs.push_back({.boundary = kStopReloadAnimationBoundary,
                               .tween = kIconAnimationTween,
                               .duration = time_scale * kIconAnimationTime});

            animated_image_container().PlayAnimation(
                {IDR_RELOAD_LOTTIE, GetForegroundColor(GetState()),
                 views::SingleAnimatedImageContainer::AnimationDirection::
                     kForward,
                 views::SingleAnimatedImageContainer::AnimationEndBehavior::
                     kReset},
                configs);
          }
        } else {
          std::vector<views::SingleAnimatedImageContainer::AnimationConfig>
              configs;
          if (visible_mode_ == Mode::kReload) {
            // Animate reload to stop to reload at normal speed.
            configs.push_back({.boundary = kReloadStopAnimationBoundary,
                               .tween = kIconAnimationTween,
                               .duration = kIconAnimationTime});
            configs.push_back({.boundary = kStopReloadAnimationBoundary,
                               .tween = kIconAnimationTween,
                               .duration = kIconAnimationTime});

          } else {
            // Animate stop to reload at a normal speed.
            configs.push_back({.boundary = kStopReloadAnimationBoundary,
                               .tween = kIconAnimationTween,
                               .duration = kIconAnimationTime});
          }
          animated_image_container().PlayAnimation(
              {IDR_RELOAD_LOTTIE, GetForegroundColor(GetState()),
               views::SingleAnimatedImageContainer::AnimationDirection::
                   kForward,
               views::SingleAnimatedImageContainer::AnimationEndBehavior::
                   kReset},
              configs);
        }
      }
      animate_transitions_ = false;
      SetVectorIcons(*reload_icon_, *reload_touch_icon_);
      break;
    case Mode::kStop:
      if (play_animation && visible_mode_ == Mode::kReload) {
        // Animate reload to stop.
        views::SingleAnimatedImageContainer::AnimationConfig config{
            .boundary = kReloadStopAnimationBoundary,
            .tween = kIconAnimationTween,
            .duration = kIconAnimationTime};

        animated_image_container().PlayAnimation(
            {IDR_RELOAD_LOTTIE, GetForegroundColor(GetState()),
             views::SingleAnimatedImageContainer::AnimationDirection::kForward,
             views::SingleAnimatedImageContainer::AnimationEndBehavior::kReset},
            config);
      }
      SetVectorIcons(*stop_icon_, *stop_touch_icon_);
      break;
  }
  visible_mode_ = mode;

  UpdateCachedTooltipText();
  OnPropertyChanged(&visible_mode_, views::PropertyEffects::kNone);
}

base::CallbackListSubscription ReloadButton::AddVisibleModeChangedCallback(
    views::PropertyChangedCallback callback) {
  return AddPropertyChangedCallback(&visible_mode_, std::move(callback));
}

void ReloadButton::ButtonPressed(const ui::Event& event) {
  // This is called in order to signal that external protocol dialogs are
  // allowed to show due to a user action, which are likely to happen on the
  // next page load after the reload button is clicked.
  // Ideally, the browser UI's event system would notify ExternalProtocolHandler
  // that a user action occurred and we are OK to open the dialog, but for some
  // reason that isn't happening every time the reload button is clicked. See
  // http://crbug.com/40180927
  ExternalProtocolHandler::PermitLaunchUrl();

  ClearPendingMenu();

  metrics_recorder_->OnButtonPressedStart(event);

  if (visible_mode_ == Mode::kStop) {
    if (command_updater_) {
      command_updater_->ExecuteCommandWithDisposition(
          IDC_STOP, WindowOpenDisposition::CURRENT_TAB);
    }
    // The user has clicked, so we can feel free to update the button, even if
    // the mouse is still hovering.
    animate_transitions_ = true;
    ChangeMode(Mode::kReload, true);
    return;
  }

  if (!double_click_timer_.IsRunning()) {
    // Shift-clicking or ctrl-clicking the reload button means we should ignore
    // any cached content.
    int command;
    int flags = event.flags();
    if (event.IsShiftDown() || event.IsControlDown()) {
      command = IDC_RELOAD_BYPASSING_CACHE;
      // Mask off Shift and Control so they don't affect the disposition below.
      flags &= ~(ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN);
    } else {
      command = IDC_RELOAD;
    }

    // Start a timer - while this timer is running, the reload button cannot be
    // changed to a stop button.  We do not set |intended_mode_| to Mode::kStop
    // here as the browser will do that when it actually starts loading (which
    // may happen synchronously, thus the need to do this before telling the
    // browser to execute the reload command).
    double_click_timer_.Start(FROM_HERE, double_click_timer_delay_, this,
                              &ReloadButton::OnDoubleClickTimer);
    animate_transitions_ = true;

    ExecuteBrowserCommand(command, flags);
    metrics_recorder_->DidExecuteReloadCommand(base::TimeTicks::Now());
    ++testing_reload_count_;
  }
}

void ReloadButton::ExecuteBrowserCommand(int command, int event_flags) {
  if (!command_updater_) {
    return;
  }
  command_updater_->ExecuteCommandWithDisposition(
      command, ui::DispositionFromEventFlags(event_flags));
}

void ReloadButton::OnDoubleClickTimer() {
  if (!IsMenuShowing()) {
    ChangeMode(intended_mode_, false);
  }
}

void ReloadButton::OnStopToReloadTimer() {
  DCHECK(!IsMenuShowing());
  ChangeMode(intended_mode_, true);
}

void ReloadButton::UpdateAccessibleHasPopup() {
  if (ShouldShowMenu()) {
    GetViewAccessibility().SetHasPopup(ax::mojom::HasPopup::kMenu);
  } else {
    GetViewAccessibility().SetHasPopup(ax::mojom::HasPopup::kNone);
  }
}

void ReloadButton::OnNextPresentation(
    const viz::FrameTimingDetails& frame_timing_details) {
  if (window_metrics_manager_) {
    window_metrics_manager_->OnReloadButtonFirstPaint(
        frame_timing_details.presentation_feedback.timestamp);
    window_metrics_manager_->OnReloadButtonFirstContentfulPaint(
        frame_timing_details.presentation_feedback.timestamp);
  }
}

DEFINE_ENUM_CONVERTERS(ReloadButton::Mode,
                       {ReloadButton::Mode::kReload, u"kReload"},
                       {ReloadButton::Mode::kStop, u"kStop"})

BEGIN_METADATA(ReloadButton)
ADD_READONLY_PROPERTY_METADATA(ReloadButton::Mode, VisibleMode)
END_METADATA
