// 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/ash/shell_delegate/tab_scrubber.h"

#include <stdint.h>

#include <algorithm>

#include "ash/shell.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_macros.h"
#include "chrome/browser/ash/browser_delegate/browser_controller.h"
#include "chrome/browser/ash/browser_delegate/browser_delegate.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/immersive/immersive_mode_controller.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/tab_strip_region_view.h"
#include "chrome/browser/ui/views/tabs/tab.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "chrome/browser/ui/views/tabs/tab_style_views.h"
#include "chrome/browser/ui/window_feature_controller/window_feature_controller.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event.h"
#include "ui/events/event_utils.h"
#include "ui/events/gesture_detection/gesture_configuration.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/public/activation_client.h"

namespace ash {

namespace {

BrowserDelegate* GetActiveBrowser() {
  BrowserDelegate* browser =
      BrowserController::GetInstance()->GetLastUsedBrowser();
  if (!browser ||
      !WindowFeatureController::From(&browser->GetBrowser())
           ->SupportsWindowFeature(
               WindowFeatureController::WindowFeature::kFeatureTabStrip) ||
      !browser->IsActive()) {
    return nullptr;
  }
  return browser;
}

views::Widget* GetWidget(BrowserDelegate* browser) {
  CHECK(browser);
  return views::Widget::GetWidgetForNativeWindow(browser->GetNativeWindow());
}

}  // namespace

// static
TabScrubber* TabScrubber::GetInstance() {
  static TabScrubber* instance = nullptr;
  if (!instance) {
    instance = new TabScrubber();
  }
  return instance;
}

// static
gfx::Point TabScrubber::GetStartPoint(TabStrip* tab_strip,
                                      int index,
                                      TabScrubber::Direction direction) {
  const Tab* tab = tab_strip->tab_at(index);
  gfx::Rect tab_bounds = tab->GetMirroredBounds();

  // Start the swipe where the tab contents start/end.  This provides a small
  // amount of slop inside the tab before a swipe will change tabs.
  auto contents_insets = tab->tab_style_views()->GetContentsInsets();
  int left = contents_insets.left();
  int right = contents_insets.right();

  // The contents insets are logical rather than physical, so reverse them for
  // RTL.
  if (base::i18n::IsRTL()) {
    std::swap(left, right);
  }

  // For very narrow tabs, the contents insets may be too large.  Clamp to the
  // opposite edges of the tab, which should be at (overlap / 2).
  gfx::Rect tab_edges = tab_bounds;
  // For odd overlap values, be conservative and inset both edges rounding up.
  tab_edges.Inset(
      gfx::Insets::VH(0, (tab->tab_style()->GetTabOverlap() + 1) / 2));
  const int x = (direction == LEFT)
                    ? std::min(tab_bounds.x() + left, tab_edges.right())
                    : std::max(tab_bounds.right() - right, tab_edges.x());

  return gfx::Point(x, tab_bounds.CenterPoint().y());
}

bool TabScrubber::IsActivationPending() {
  return activate_timer_.IsRunning();
}

void TabScrubber::SetEnabled(bool enabled) {
  enabled_ = enabled;
}

void TabScrubber::SynthesizedScrollEvent(float x_offset,
                                         bool is_fling_scroll_event) {
  // EventType::kScrollFlingStart and EventType::kScrollFlingCancel are both
  // handled in the same way inside OnScrollEvent(), so we can set
  // EventType::kScrollFlingStart if `is_fling_scroll_event` is true.
  // TODO(crbug.com/40207972): Instead of generating event here, use the real
  // event passed from wayland.
  ui::EventType event_type = is_fling_scroll_event
                                 ? ui::EventType::kScrollFlingStart
                                 : ui::EventType::kScroll;
  // Set `y_offset` as zero so that its absolute value is always not larger than
  // that of `x_offset` to represent the horizontal scroll.
  constexpr float y_offset = 0.f;
  ui::ScrollEvent event(event_type, gfx::PointF(), gfx::PointF(),
                        ui::EventTimeForNow(),
                        /*flags=*/0, x_offset, y_offset,
                        /*x_offset_ordinal=*/0.f,
                        /*y_offset_ordinal=*/0.f, kFingerCount);
  OnScrollEvent(&event);
}

TabScrubber::TabScrubber() {
  ash::Shell::Get()->AddPreTargetHandler(this);
  browser_controller_observation_.Observe(BrowserController::GetInstance());
}

TabScrubber::~TabScrubber() = default;

void TabScrubber::OnScrollEvent(ui::ScrollEvent* event) {
  if (!enabled_) {
    return;
  }

  if (event->IsFlingScrollEvent()) {
    // If we are not scrubbing, do not stop mark the event as handled here so
    // that other events can consume it.
    if (FinishScrub(true)) {
      event->SetHandled();
    }
    immersive_reveal_lock_.reset();
    return;
  }

  if (event->finger_count() != kFingerCount) {
    return;
  }

  BrowserDelegate* browser = GetActiveBrowser();
  if (!browser || (scrubbing_ && browser_ && browser != browser_) ||
      (highlighted_tab_ != -1 && static_cast<size_t>(highlighted_tab_) >=
                                     browser->GetWebContentsCount())) {
    if (FinishScrub(false)) {
      event->SetHandled();
    }
    return;
  }

  if (tab_strip_ && tab_strip_->IsAnimatingInTabStrip()) {
    if (FinishScrub(false)) {
      event->SetHandled();
    }
    return;
  }

  // If the scroll is vertical, do not start scrubbing.
  if (!scrubbing_ &&
      std::abs(event->x_offset()) < std::abs(event->y_offset())) {
    return;
  }

  // We are handling the event.
  event->SetHandled();

  // The event's x_offset doesn't change in an RTL layout. Negative value means
  // left, positive means right.
  BrowserView* browser_view =
      BrowserView::GetBrowserViewForBrowser(&browser->GetBrowser());

  if (browser_view->ShouldDrawVerticalTabStrip()) {
    // TODO(crbug.com/484364227): TabScrubbing is not supported in VerticalTabs
    // at this point in time.
    return;
  }

  float x_offset = event->x_offset();
  if (!scrubbing_) {
    BeginScrub(browser_view, x_offset);
  } else if (highlighted_tab_ == -1) {
    // Has the direction of the swipe changed while scrubbing?
    Direction direction = (x_offset < 0) ? LEFT : RIGHT;
    if (direction != swipe_direction_) {
      ScrubDirectionChanged(direction);
    }
  }

  UpdateSwipeX(x_offset);

  Tab* new_tab = tab_strip_->GetTabAt(gfx::Point(swipe_x_, swipe_y_));
  if (!new_tab) {
    return;
  }

  std::optional<int> new_index = tab_strip_->GetModelIndexOf(new_tab);
  if (!new_index) {
    return;
  }

  if (highlighted_tab_ == -1 &&
      new_index.value() ==
          browser_->GetBrowser().GetTabStripModel()->active_index()) {
    return;
  }

  if (new_index.value() != highlighted_tab_) {
    if (activate_timer_.IsRunning()) {
      activate_timer_.Reset();
    } else {
      ScheduleFinishScrubIfNeeded();
    }
  }

  UpdateHighlightedTab(new_tab, new_index.value());

  if (highlighted_tab_ != -1) {
    gfx::Point hover_point(swipe_x_, swipe_y_);
    views::View::ConvertPointToTarget(tab_strip_, new_tab, &hover_point);
  }
}

void TabScrubber::OnBrowserClosed(BrowserDelegate* browser) {
  if (browser != browser_) {
    return;
  }

  if (browser_) {
    GetWidget(browser_)->ReleaseCapture();
  }

  activate_timer_.Stop();
  swipe_x_ = -1;
  swipe_y_ = -1;
  scrubbing_ = false;
  highlighted_tab_ = -1;
  browser_ = nullptr;
  tab_strip_ = nullptr;
}

void TabScrubber::OnTabAdded(int index) {
  if (highlighted_tab_ == -1) {
    return;
  }

  if (index < highlighted_tab_) {
    ++highlighted_tab_;
  }
}

void TabScrubber::OnTabMoved(int from_index, int to_index) {
  if (highlighted_tab_ == -1) {
    return;
  }

  if (from_index == highlighted_tab_) {
    highlighted_tab_ = to_index;
  } else if (from_index < highlighted_tab_ && highlighted_tab_ <= to_index) {
    --highlighted_tab_;
  } else if (from_index > highlighted_tab_ && highlighted_tab_ >= to_index) {
    ++highlighted_tab_;
  }
}

void TabScrubber::OnTabRemoved(int index) {
  if (highlighted_tab_ == -1) {
    return;
  }
  if (index == highlighted_tab_) {
    FinishScrub(false);
    return;
  }
  if (index < highlighted_tab_) {
    --highlighted_tab_;
  }
}

void TabScrubber::BeginScrub(BrowserView* browser_view, float x_offset) {
  DCHECK(browser_view);
  DCHECK(browser_view->browser());

  if (browser_view->ShouldDrawVerticalTabStrip()) {
    // TODO(crbug.com/484364227): TabScrubbing is not supported in VerticalTabs
    // at this point in time.
    return;
  }

  scrubbing_start_time_ = base::TimeTicks::Now();
  // TODO(crbug.com/465835455): Move TabScrubber into
  // HorizontalTabStripRegionView since the current implementation won't work
  // for Vertical Tabs.
  tab_strip_ = views::AsViewClass<TabStrip>(
      browser_view->tab_strip_view()->GetTabStripView());
  scrubbing_ = true;
  browser_ =
      BrowserController::GetInstance()->GetDelegate(browser_view->browser());

  Direction direction = (x_offset < 0) ? LEFT : RIGHT;
  ScrubDirectionChanged(direction);

  auto* const immersive_controller =
      ImmersiveModeController::From(&browser_->GetBrowser());
  if (immersive_controller->IsEnabled()) {
    immersive_reveal_lock_ = immersive_controller->GetRevealedLock(
        ImmersiveModeController::ANIMATE_REVEAL_YES);
  }

  browser_view->tab_strip_view()->SetTabStripObserver(this);

  // Capture the event so that the scroll event will not be handled by other
  // clients. This is required to work well with overview mode gesture.
  GetWidget(browser_)->SetCapture(/*view=*/nullptr);
}

bool TabScrubber::FinishScrub(bool activate) {
  const int stops_scrubbing = scrubbing_;
  activate_timer_.Stop();

  if (browser_ && browser_->GetWindow()) {
    GetWidget(browser_)->ReleaseCapture();

    if (activate && highlighted_tab_ != -1) {
      Tab* tab = tab_strip_->tab_at(highlighted_tab_);
      tab->HideHover(TabStyle::HideHoverStyle::kImmediate);
      int distance =
          std::abs(highlighted_tab_ -
                   browser_->GetBrowser().GetTabStripModel()->active_index());
      UMA_HISTOGRAM_CUSTOM_COUNTS("Tabs.ScrubDistance", distance, 1, 20, 21);
      UMA_HISTOGRAM_TIMES("Tabs.ScrubDuration",
                          base::TimeTicks::Now() - scrubbing_start_time_);
      browser_->GetBrowser().GetTabStripModel()->ActivateTabAt(
          highlighted_tab_,
          TabStripUserGestureDetails(
              TabStripUserGestureDetails::GestureType::kOther));
    }

    BrowserView* browser_view =
        BrowserView::GetBrowserViewForBrowser(&browser_->GetBrowser());
    TabStripRegionView* tab_strip_view = browser_view->tab_strip_view();
    tab_strip_view->SetTabStripObserver(nullptr);
  }

  browser_ = nullptr;
  tab_strip_ = nullptr;
  swipe_x_ = -1;
  swipe_y_ = -1;
  scrubbing_ = false;
  highlighted_tab_ = -1;

  return stops_scrubbing;
}

void TabScrubber::ScheduleFinishScrubIfNeeded() {
  // Tests use a really long delay to ensure RunLoops don't unnecessarily
  // trigger the timer running.
  const base::TimeDelta delay =
      base::Milliseconds(use_default_activation_delay_ ? 200 : 20000);
  activate_timer_.Start(
      FROM_HERE, delay,
      base::BindRepeating(base::IgnoreResult(&TabScrubber::FinishScrub),
                          base::Unretained(this), true));
}

void TabScrubber::ScrubDirectionChanged(Direction direction) {
  DCHECK(browser_);
  DCHECK(tab_strip_);
  DCHECK(scrubbing_);

  swipe_direction_ = direction;
  const gfx::Point start_point = GetStartPoint(
      tab_strip_, browser_->GetBrowser().GetTabStripModel()->active_index(),
      swipe_direction_);
  swipe_x_ = start_point.x();
  swipe_y_ = start_point.y();
}

void TabScrubber::UpdateSwipeX(float x_offset) {
  DCHECK(browser_);
  DCHECK(tab_strip_);
  DCHECK(scrubbing_);

  // Make the swipe speed inversely proportional with the number or tabs:
  // Each added tab introduces a reduction of 2% in |x_offset|, with a value of
  // one fourth of |x_offset| as the minimum (i.e. we need 38 tabs to reach
  // that minimum reduction). Please note, x_offset might be negative.
  float min = 0.25 * x_offset;
  float max = x_offset;
  if (x_offset < 0) {
    std::swap(min, max);
  }
  swipe_x_ += std::clamp(
      x_offset - (tab_strip_->GetTabCount() * 0.02f * x_offset), min, max);

  // In an RTL layout, everything is mirrored, i.e. the index of the first tab
  // (with the smallest X mirrored co-ordinates) is actually the index of the
  // last tab. Same for the index of the last tab.
  int first_tab_index = base::i18n::IsRTL() ? tab_strip_->GetTabCount() - 1 : 0;
  int last_tab_index = base::i18n::IsRTL() ? 0 : tab_strip_->GetTabCount() - 1;

  Tab* first_tab = tab_strip_->tab_at(first_tab_index);
  int first_tab_center = first_tab->GetMirroredBounds().CenterPoint().x();
  Tab* last_tab = tab_strip_->tab_at(last_tab_index);
  int last_tab_center = last_tab->GetMirroredBounds().CenterPoint().x();

  swipe_x_ = std::clamp(swipe_x_, static_cast<float>(first_tab_center),
                        static_cast<float>(last_tab_center));
}

void TabScrubber::UpdateHighlightedTab(Tab* new_tab, int new_index) {
  DCHECK(scrubbing_);
  DCHECK(new_tab);

  if (new_index == highlighted_tab_) {
    return;
  }

  if (highlighted_tab_ != -1) {
    Tab* tab = tab_strip_->tab_at(highlighted_tab_);
    tab->HideHover(TabStyle::HideHoverStyle::kImmediate);
  }

  if (new_index != browser_->GetBrowser().GetTabStripModel()->active_index()) {
    highlighted_tab_ = new_index;
    new_tab->ShowHover(TabStyle::ShowHoverStyle::kPronounced);
  } else {
    highlighted_tab_ = -1;
  }
}

}  // namespace ash
