// Copyright 2019 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/tabs/tab_strip_layout_helper.h"

#include <algorithm>
#include <optional>
#include <utility>

#include "base/memory/raw_ptr.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/tab_types.h"
#include "chrome/browser/ui/views/tabs/tab.h"
#include "chrome/browser/ui/views/tabs/tab_group_header.h"
#include "chrome/browser/ui/views/tabs/tab_layout_state.h"
#include "chrome/browser/ui/views/tabs/tab_slot_view.h"
#include "chrome/browser/ui/views/tabs/tab_strip_layout_types.h"
#include "chrome/browser/ui/views/tabs/tab_style_views.h"
#include "components/split_tabs/split_tab_id.h"
#include "tab_container_controller.h"
#include "ui/gfx/range/range.h"
#include "ui/views/view_model.h"
#include "ui/views/view_utils.h"

struct TabStripLayoutHelper::TabSlot {
  static TabStripLayoutHelper::TabSlot CreateForTabSlotView(TabSlotView* view,
                                                            TabPinned pinned) {
    TabStripLayoutHelper::TabSlot slot;
    slot.type = view->GetTabSlotViewType();
    slot.view = view;
    slot.state = TabLayoutState(TabOpen::kOpen, pinned, TabActive::kInactive,
                                view->split());
    return slot;
  }

  TabSlotView::ViewType type;
  raw_ptr<TabSlotView, DanglingUntriaged> view;
  TabLayoutState state;
};

TabStripLayoutHelper::TabStripLayoutHelper(
    const TabContainerController& controller,
    GetTabsCallback get_tabs_callback)
    : controller_(controller),
      get_tabs_callback_(get_tabs_callback),
      tab_strip_layout_domain_(LayoutDomain::kInactiveWidthEqualsActiveWidth) {}

TabStripLayoutHelper::~TabStripLayoutHelper() = default;

std::vector<Tab*> TabStripLayoutHelper::GetTabs() const {
  std::vector<Tab*> tabs;
  for (const TabSlot& slot : slots_) {
    if (slot.type == TabSlotView::ViewType::kTab) {
      tabs.push_back(views::AsViewClass<Tab>(slot.view));
    }
  }

  return tabs;
}

std::vector<TabSlotView*> TabStripLayoutHelper::GetTabSlotViews() const {
  std::vector<TabSlotView*> views;
  for (const TabSlot& slot : slots_) {
    views.push_back(slot.view);
  }
  return views;
}

size_t TabStripLayoutHelper::GetPinnedTabCount() const {
  size_t pinned_count = 0;
  for (const TabSlot& slot : slots_) {
    if (slot.state.pinned() == TabPinned::kPinned && !slot.state.IsClosed()) {
      pinned_count++;
    }
  }
  return pinned_count;
}

void TabStripLayoutHelper::InsertTabAt(int model_index,
                                       Tab* tab,
                                       TabPinned pinned) {
  const int slot_index =
      GetSlotInsertionIndexForNewTab(model_index, tab->group());
  slots_.insert(slots_.begin() + slot_index,
                TabSlot::CreateForTabSlotView(tab, pinned));
}

void TabStripLayoutHelper::MarkTabAsClosing(int model_index, Tab* tab) {
  const int slot_index = GetSlotIndexForExistingTab(model_index);
  slots_[slot_index].state.set_open(TabOpen::kClosed);
}

void TabStripLayoutHelper::RemoveTab(Tab* tab) {
  auto it = std::ranges::find_if(slots_, [tab](const TabSlot& slot) {
    return slot.type == TabSlotView::ViewType::kTab && slot.view == tab;
  });
  if (it != slots_.end()) {
    slots_.erase(it);
  }
}

void TabStripLayoutHelper::MoveTab(
    std::optional<tab_groups::TabGroupId> moving_tab_group,
    int prev_index,
    int new_index) {
  const int prev_slot_index = GetSlotIndexForExistingTab(prev_index);
  TabSlot moving_tab = slots_[prev_slot_index];
  slots_.erase(slots_.begin() + prev_slot_index);

  const int new_slot_index =
      GetSlotInsertionIndexForNewTab(new_index, moving_tab_group);
  slots_.insert(slots_.begin() + new_slot_index, moving_tab);

  if (moving_tab_group.has_value()) {
    UpdateGroupHeaderIndex(moving_tab_group.value());
  }
}

void TabStripLayoutHelper::SetTabPinned(int model_index, TabPinned pinned) {
  const int slot_index = GetSlotIndexForExistingTab(model_index);
  slots_[slot_index].state.set_pinned(pinned);
}

void TabStripLayoutHelper::InsertGroupHeader(tab_groups::TabGroupId group,
                                             TabGroupHeader* header) {
  gfx::Range tabs_in_group = controller_->ListTabsInGroup(group);
  const int header_slot_index =
      GetSlotInsertionIndexForNewTab(tabs_in_group.start(), group);
  slots_.insert(slots_.begin() + header_slot_index,
                TabSlot::CreateForTabSlotView(header, TabPinned::kUnpinned));

  // Set the starting location of the header to something reasonable for the
  // animation.
  slots_[header_slot_index].view->SetBoundsRect(
      GetTabs()[tabs_in_group.start()]->bounds());
}

void TabStripLayoutHelper::RemoveGroupHeader(tab_groups::TabGroupId group) {
  const int slot_index = GetSlotIndexForGroupHeader(group);
  slots_.erase(slots_.begin() + slot_index);
}

void TabStripLayoutHelper::UpdateGroupHeaderIndex(
    tab_groups::TabGroupId group) {
  if (!GetFirstTabSlotForGroup(group).has_value()) {
    return;
  }

  const int slot_index = GetSlotIndexForGroupHeader(group);
  TabSlot header_slot = slots_[slot_index];

  slots_.erase(slots_.begin() + slot_index);

  // Recalculate the first tab index after removing the header as it
  // helps with the header insertion index calculation.
  std::optional<int> first_tab = GetFirstTabSlotForGroup(group);

  slots_.insert(slots_.begin() + first_tab.value(), header_slot);
}

void TabStripLayoutHelper::SetActiveTab(
    std::optional<size_t> prev_active_index,
    std::optional<size_t> new_active_index) {
  if (prev_active_index.has_value()) {
    const int prev_slot_index =
        GetSlotIndexForExistingTab(prev_active_index.value());
    slots_[prev_slot_index].state.set_active(TabActive::kInactive);
    // We are assuming that when a tabs active state is changed, it can't be in
    // the middle of a move or something that would make them non-contiguous.
    std::optional<int> adjacent_split = GetAdjacentSplitTab(prev_slot_index);
    if (adjacent_split.has_value()) {
      slots_[adjacent_split.value()].state.set_active(TabActive::kInactive);
    }
  }
  if (new_active_index.has_value()) {
    const int new_slot_index =
        GetSlotIndexForExistingTab(new_active_index.value());
    slots_[new_slot_index].state.set_active(TabActive::kActive);
    std::optional<int> adjacent_split = GetAdjacentSplitTab(new_slot_index);
    if (adjacent_split.has_value()) {
      slots_[adjacent_split.value()].state.set_active(TabActive::kActive);
    }
  }
}

int TabStripLayoutHelper::CalculateMinimumWidth() {
  auto [bounds, layout_domain] = CalculateIdealBounds(0);

  return bounds.empty() ? 0 : bounds.back().right();
}

int TabStripLayoutHelper::CalculatePreferredWidth() {
  auto [bounds, layout_domain] = CalculateIdealBounds(std::nullopt);

  return bounds.empty() ? 0 : bounds.back().right();
}

int TabStripLayoutHelper::UpdateIdealBounds(int available_width) {
  auto [bounds, layout_domain] = CalculateIdealBounds(available_width);
  DCHECK_EQ(slots_.size(), bounds.size());
  tab_strip_layout_domain_ = layout_domain;

  views::ViewModelT<Tab>* tabs = get_tabs_callback_.Run();
  const std::optional<int> active_tab_model_index =
      controller_->GetActiveIndex();
  const std::optional<int> active_tab_slot_index =
      active_tab_model_index.has_value()
          ? std::optional<int>(
                GetSlotIndexForExistingTab(active_tab_model_index.value()))
          : std::nullopt;

  // Store the active split (if applicable) for determining whether other tabs
  // in the split should be active.
  std::optional<split_tabs::SplitTabId> active_split_id;
  if (active_tab_slot_index.has_value()) {
    const TabSlot& active_slot = slots_[active_tab_slot_index.value()];
    active_split_id = active_slot.view->split();
  }

  int current_tab_model_index = 0;
  for (int i = 0; i < static_cast<int>(bounds.size()); ++i) {
    const TabSlot& slot = slots_[i];
    switch (slot.type) {
      case TabSlotView::ViewType::kTab:
        if (!slot.state.IsClosed()) {
          tabs->set_ideal_bounds(current_tab_model_index, bounds[i]);
          ++current_tab_model_index;
        }
        break;
      case TabSlotView::ViewType::kTabGroupHeader:
        group_header_ideal_bounds_[slot.view->group().value()] = bounds[i];
        break;
    }
  }

  return bounds.back().right();
}

std::pair<std::vector<gfx::Rect>, LayoutDomain>
TabStripLayoutHelper::CalculateIdealBounds(std::optional<int> available_width) {
  const std::optional<int> active_tab_model_index =
      controller_->GetActiveIndex();
  const std::optional<int> active_tab_slot_index =
      active_tab_model_index.has_value()
          ? std::optional<int>(
                GetSlotIndexForExistingTab(active_tab_model_index.value()))
          : std::nullopt;
  const std::optional<int> active_split_tab_slot_index =
      active_tab_slot_index.has_value()
          ? GetAdjacentSplitTab(active_tab_slot_index.value())
          : std::nullopt;
  const int pinned_tab_count = GetPinnedTabCount();
  const std::optional<int> last_pinned_tab_slot_index =
      pinned_tab_count > 0
          ? std::optional<int>(GetSlotIndexForExistingTab(pinned_tab_count - 1))
          : std::nullopt;

  std::vector<TabWidthConstraints> tab_widths;
  for (int i = 0; i < static_cast<int>(slots_.size()); i++) {
    auto active =
        (i == active_tab_slot_index || i == active_split_tab_slot_index)
            ? TabActive::kActive
            : TabActive::kInactive;
    auto pinned = last_pinned_tab_slot_index.has_value() &&
                          i <= last_pinned_tab_slot_index
                      ? TabPinned::kPinned
                      : TabPinned::kUnpinned;

    // A collapsed tab animates closed like a closed tab.
    auto open = (slots_[i].state.IsClosed() || SlotIsCollapsedTab(i))
                    ? TabOpen::kClosed
                    : TabOpen::kOpen;
    TabLayoutState state =
        TabLayoutState(open, pinned, active, slots_[i].view->split());
    TabSizeInfo size_info = slots_[i].view->GetTabSizeInfo();

    tab_widths.emplace_back(state, size_info);
  }

  return CalculateTabBounds(tab_widths, available_width);
}

int TabStripLayoutHelper::GetSlotIndexForExistingTab(int model_index) const {
  const int original_slot_index =
      GetFirstSlotIndexForTabModelIndex(model_index);
  CHECK_LT(original_slot_index, static_cast<int>(slots_.size()))
      << "model_index = " << model_index
      << " does not represent an existing tab";

  int slot_index = original_slot_index;

  if (slots_[slot_index].type == TabSlotView::ViewType::kTab) {
    CHECK(!slots_[slot_index].state.IsClosed());
    return slot_index;
  }

  // If `slot_index` is a group header we must return the next slot that
  // is not animating closed.
  if (slots_[slot_index].type == TabSlotView::ViewType::kTabGroupHeader) {
    // Skip all slots animating closed.
    do {
      slot_index += 1;
    } while (slot_index < static_cast<int>(slots_.size()) &&
             slots_[slot_index].state.IsClosed());

    // Double check we arrived at a tab.
    CHECK_LT(slot_index, static_cast<int>(slots_.size()))
        << "group header at " << original_slot_index
        << " not followed by an open tab";
    CHECK_EQ(slots_[slot_index].type, TabSlotView::ViewType::kTab);
  }

  return slot_index;
}

int TabStripLayoutHelper::GetSlotInsertionIndexForNewTab(
    int new_model_index,
    std::optional<tab_groups::TabGroupId> group) const {
  int slot_index = GetFirstSlotIndexForTabModelIndex(new_model_index);

  if (slot_index == static_cast<int>(slots_.size())) {
    return slot_index;
  }

  // If `slot_index` points to a group header and the new tab's `group`
  // matches, the tab goes to the right of the header to keep it
  // contiguous.
  if (slots_[slot_index].type == TabSlotView::ViewType::kTabGroupHeader &&
      views::AsViewClass<const TabGroupHeader>(slots_[slot_index].view)
              ->group() == group) {
    return slot_index + 1;
  }

  return slot_index;
}

std::optional<int> TabStripLayoutHelper::GetFirstTabSlotForGroup(
    tab_groups::TabGroupId group) const {
  for (int slot_index = 0; slot_index < static_cast<int>(slots_.size());
       ++slot_index) {
    if (slots_[slot_index].state.IsClosed()) {
      continue;
    }

    if (slots_[slot_index].type == TabSlotView::ViewType::kTab &&
        slots_[slot_index].view->group().has_value() &&
        slots_[slot_index].view->group().value() == group) {
      return slot_index;
    }
  }

  return std::nullopt;
}

int TabStripLayoutHelper::GetFirstSlotIndexForTabModelIndex(
    int model_index) const {
  int current_model_index = 0;

  // Conceptually we assign a model index to each slot equal to the
  // number of open tabs preceding it. Group headers will have the same
  // index as the tab before it, and each open tab will have the index
  // of the previous slot plus 1. Closing tabs are not counted, and are
  // skipped altogether.
  //
  // We simply return the first slot that has a matching model index.
  for (int slot_index = 0; slot_index < static_cast<int>(slots_.size());
       ++slot_index) {
    if (slots_[slot_index].state.IsClosed()) {
      continue;
    }

    if (model_index == current_model_index) {
      return slot_index;
    }

    if (slots_[slot_index].type == TabSlotView::ViewType::kTab) {
      current_model_index += 1;
    }
  }

  // If there's no slot in `slots_` corresponding to `model_index`, then
  // `model_index` may represent the first tab past the end of the
  // tabstrip. In this case we should return the first-past-the-end
  // index in `slots_`.
  CHECK_EQ(current_model_index, model_index) << "model_index is too large";
  return slots_.size();
}

int TabStripLayoutHelper::GetSlotIndexForGroupHeader(
    tab_groups::TabGroupId group) const {
  const auto it = std::ranges::find_if(slots_, [group](const auto& slot) {
    return slot.type == TabSlotView::ViewType::kTabGroupHeader &&
           views::AsViewClass<TabGroupHeader>(slot.view)->group() == group;
  });
  CHECK(it != slots_.end());
  return it - slots_.begin();
}

std::optional<int> TabStripLayoutHelper::GetAdjacentSplitTab(
    int slot_index) const {
  const std::optional<split_tabs::SplitTabId> split_id =
      slots_[slot_index].view->split();
  if (!split_id.has_value()) {
    return std::nullopt;
  } else if (slot_index > 0 &&
             slots_[slot_index - 1].view->split() == split_id) {
    return slot_index - 1;
  } else if (slot_index < static_cast<int>(slots_.size()) - 1 &&
             slots_[slot_index + 1].view->split() == split_id) {
    return slot_index + 1;
  }
  return std::nullopt;
}

bool TabStripLayoutHelper::SlotIsCollapsedTab(int i) const {
  const std::optional<tab_groups::TabGroupId> focused_group =
      controller_->GetFocusedGroup();

  // If a group is focused, all other tabs and group headers should be
  // collapsed.
  if (focused_group.has_value()) {
    // Pinned tabs should not be collapsed.
    if (slots_[i].state.pinned() == TabPinned::kPinned) {
      return false;
    }

    const std::optional<tab_groups::TabGroupId> id = slots_[i].view->group();
    return !id.has_value() || id.value() != focused_group.value();
  }

  // Otherwise, a tab is only collapsed if it is in an explicitly collapsed
  // group.
  const std::optional<tab_groups::TabGroupId> id = slots_[i].view->group();
  return slots_[i].type == TabSlotView::ViewType::kTab && id.has_value() &&
         controller_->IsGroupCollapsed(id.value());
}
