// 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 "chrome/browser/ui/views/tabs/stacked_tab_strip_layout.h"

#include <stdio.h>

#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "content/public/browser/user_metrics.h"

using base::UserMetricsAction;

StackedTabStripLayout::StackedTabStripLayout(const gfx::Size& size,
                                             int overlap,
                                             int stacked_padding,
                                             int max_stacked_count,
                                             views::ViewModelBase* view_model)
    : size_(size),
      overlap_(overlap),
      stacked_padding_(stacked_padding),
      max_stacked_count_(max_stacked_count),
      view_model_(view_model),
      x_(0),
      width_(0),
      pinned_tab_count_(0),
      pinned_tab_to_non_pinned_tab_(0),
      active_index_(-1),
      first_tab_x_(0) {
}

StackedTabStripLayout::~StackedTabStripLayout() {
}

void StackedTabStripLayout::SetXAndPinnedCount(int x, int pinned_tab_count) {
  first_tab_x_ = x;
  x_ = x;
  pinned_tab_count_ = pinned_tab_count;
  pinned_tab_to_non_pinned_tab_ = 0;
  if (!requires_stacking() || tab_count() == pinned_tab_count) {
    ResetToIdealState();
    return;
  }
  if (pinned_tab_count > 0) {
    pinned_tab_to_non_pinned_tab_ = x - ideal_x(pinned_tab_count - 1);
    first_tab_x_ = ideal_x(0);
  }
  SetIdealBoundsAt(active_index(), ConstrainActiveX(ideal_x(active_index())));
  LayoutByTabOffsetAfter(active_index());
  LayoutByTabOffsetBefore(active_index());
}

void StackedTabStripLayout::SetWidth(int width) {
  if (width_ == width)
    return;

  width_ = width;
  if (!requires_stacking()) {
    ResetToIdealState();
    return;
  }
  SetActiveBoundsAndLayoutFromActiveTab();
}

void StackedTabStripLayout::SetActiveIndex(int index) {
  int old = active_index();
  active_index_ = index;
  if (old == active_index() || !requires_stacking())
    return;
  SetIdealBoundsAt(active_index(), ConstrainActiveX(ideal_x(active_index())));
  LayoutByTabOffsetBefore(active_index());
  LayoutByTabOffsetAfter(active_index());
  AdjustStackedTabs();
}

void StackedTabStripLayout::DragActiveTab(int delta) {
  if (delta == 0 || !requires_stacking())
    return;

  content::RecordAction(UserMetricsAction("StackedTab_DragActiveTab"));
  int initial_x = ideal_x(active_index());
  // If we're at a particular edge and start dragging, expose all the tabs after
  // the tab (or before when dragging to the left).
  if (delta > 0 && initial_x == GetMinX(active_index())) {
    LayoutByTabOffsetAfter(active_index());
    AdjustStackedTabs();
  } else if (delta < 0 && initial_x == GetMaxX(active_index())) {
    LayoutByTabOffsetBefore(active_index());
    ResetToIdealState();
  }
  int x = delta > 0 ?
      std::min(initial_x + delta, GetMaxDragX(active_index())) :
      std::max(initial_x + delta, GetMinDragX(active_index()));
  if (x != initial_x) {
    SetIdealBoundsAt(active_index(), x);
    if (delta > 0) {
      PushTabsAfter(active_index(), (x - initial_x));
      LayoutForDragBefore(active_index());
    } else {
      PushTabsBefore(active_index(), initial_x - x);
      LayoutForDragAfter(active_index());
    }
    delta -= (x - initial_x);
  }
  if (delta > 0)
    ExpandTabsBefore(active_index(), delta);
  else if (delta < 0)
    ExpandTabsAfter(active_index(), -delta);
  AdjustStackedTabs();
}

void StackedTabStripLayout::SizeToFit() {
  if (!tab_count())
    return;

  if (!requires_stacking()) {
    ResetToIdealState();
    return;
  }

  if (ideal_x(0) != first_tab_x_) {
    // Tabs have been dragged to the right. Pull in the tabs from left to right
    // to fill in space.
    int delta = ideal_x(0) - first_tab_x_;
    int i = 0;
    for (; i < pinned_tab_count_; ++i) {
      gfx::Rect pinned_bounds(view_model_->ideal_bounds(i));
      pinned_bounds.set_x(ideal_x(i) - delta);
      view_model_->set_ideal_bounds(i, pinned_bounds);
    }
    for (; delta > 0 && i < tab_count() - 1; ++i) {
      const int exposed = tab_offset() - (ideal_x(i + 1) - ideal_x(i));
      SetIdealBoundsAt(i, ideal_x(i) - delta);
      delta -= exposed;
    }
    AdjustStackedTabs();
    return;
  }

  const int max_x = width_ - size_.width();
  if (ideal_x(tab_count() - 1) == max_x)
    return;

  // Tabs have been dragged to the left. Pull in tabs from right to left to fill
  // in space.
  SetIdealBoundsAt(tab_count() - 1, max_x);
  for (int i = tab_count() - 2; i > pinned_tab_count_ &&
           ideal_x(i + 1) - ideal_x(i) > tab_offset(); --i) {
    SetIdealBoundsAt(i, ideal_x(i + 1) - tab_offset());
  }
  AdjustStackedTabs();
}

void StackedTabStripLayout::AddTab(int index, int add_types, int start_x) {
  if (add_types & kAddTypeActive)
    active_index_ = index;
  else if (active_index_ >= index)
    active_index_++;
  if (add_types & kAddTypePinned)
    pinned_tab_count_++;
  x_ = start_x;
  if (!requires_stacking() || normal_tab_count() <= 1) {
    ResetToIdealState();
    return;
  }
  int active_x = (index + 1 == tab_count()) ?
      width_ - size_.width() : ideal_x(index + 1);
  SetIdealBoundsAt(active_index(), ConstrainActiveX(active_x));
  LayoutByTabOffsetAfter(active_index());
  LayoutByTabOffsetBefore(active_index());
  AdjustStackedTabs();

  if ((add_types & kAddTypeActive) == 0)
    MakeVisible(index);
}

void StackedTabStripLayout::RemoveTab(int index, int start_x, int old_x) {
  if (index == active_index_)
    active_index_ = std::min(active_index_, tab_count() - 1);
  else if (index < active_index_)
    active_index_--;
  bool removed_pinned_tab = index < pinned_tab_count_;
  if (removed_pinned_tab) {
    pinned_tab_count_--;
    DCHECK_GE(pinned_tab_count_, 0);
  }
  int delta = start_x - x_;
  x_ = start_x;
  if (!requires_stacking()) {
    ResetToIdealState();
    return;
  }
  if (removed_pinned_tab) {
    for (int i = pinned_tab_count_; i < tab_count(); ++i)
      SetIdealBoundsAt(i, ideal_x(i) + delta);
  }
  SetActiveBoundsAndLayoutFromActiveTab();
  AdjustStackedTabs();
}

void StackedTabStripLayout::MoveTab(int from,
                                    int to,
                                    int new_active_index,
                                    int start_x,
                                    int pinned_tab_count) {
  x_ = start_x;
  pinned_tab_count_ = pinned_tab_count;
  active_index_ = new_active_index;
  if (!requires_stacking() || tab_count() == pinned_tab_count_) {
    ResetToIdealState();
  } else {
    SetIdealBoundsAt(active_index(),
                     ConstrainActiveX(ideal_x(active_index())));
    LayoutByTabOffsetAfter(active_index());
    LayoutByTabOffsetBefore(active_index());
    AdjustStackedTabs();
  }
  pinned_tab_to_non_pinned_tab_ = pinned_tab_count > 0 ?
      start_x - ideal_x(pinned_tab_count - 1) : 0;
  first_tab_x_ = pinned_tab_count > 0 ? ideal_x(0) : start_x;
}

bool StackedTabStripLayout::IsStacked(int index) const {
  if (index == active_index() || tab_count() == pinned_tab_count_ ||
      index < pinned_tab_count_)
    return false;
  if (index > active_index())
    return ideal_x(index) != ideal_x(index - 1) + tab_offset();
  return ideal_x(index + 1) != ideal_x(index) + tab_offset();
}

void StackedTabStripLayout::SetActiveTabLocation(int x) {
  if (!requires_stacking())
    return;

  const int index = active_index();
  if (index <= pinned_tab_count_)
    return;

  x = std::min(GetMaxX(index), std::max(x, GetMinX(index)));
  if (x == ideal_x(index))
    return;

  SetIdealBoundsAt(index, x);
  LayoutByTabOffsetBefore(index);
  LayoutByTabOffsetAfter(index);
}

#if !defined(NDEBUG)
std::string StackedTabStripLayout::BoundsString() const {
  std::string result;
  for (int i = 0; i < view_model_->view_size(); ++i) {
    if (!result.empty())
      result += " ";
    if (i == active_index())
      result += "[";
    result += base::IntToString(view_model_->ideal_bounds(i).x());
    if (i == active_index())
      result += "]";
  }
  return result;
}
#endif

void StackedTabStripLayout::Reset(int x,
                                  int width,
                                  int pinned_tab_count,
                                  int active_index) {
  x_ = x;
  width_ = width;
  pinned_tab_count_ = pinned_tab_count;
  pinned_tab_to_non_pinned_tab_ = pinned_tab_count > 0 ?
      x - ideal_x(pinned_tab_count - 1) : 0;
  first_tab_x_ = pinned_tab_count > 0 ? ideal_x(0) : x;
  active_index_ = active_index;
  ResetToIdealState();
}

void StackedTabStripLayout::ResetToIdealState() {
  if (tab_count() == pinned_tab_count_)
    return;

  if (!requires_stacking()) {
    SetIdealBoundsAt(pinned_tab_count_, x_);
    LayoutByTabOffsetAfter(pinned_tab_count_);
    return;
  }

  if (normal_tab_count() == 1) {
    // TODO: might want to shrink the tab here.
    SetIdealBoundsAt(pinned_tab_count_, 0);
    return;
  }

  int available_width = width_ - x_;
  int leading_count = active_index() - pinned_tab_count_;
  int trailing_count = tab_count() - active_index();
  if (width_for_count(leading_count + 1) + max_stacked_width() <
      available_width) {
    SetIdealBoundsAt(pinned_tab_count_, x_);
    LayoutByTabOffsetAfter(pinned_tab_count_);
  } else if (width_for_count(trailing_count) + max_stacked_width() <
             available_width) {
    SetIdealBoundsAt(tab_count() - 1, width_ - size_.width());
    LayoutByTabOffsetBefore(tab_count() - 1);
  } else {
    int index = active_index();
    do {
      int stacked_padding =
          stacked_padding_for_count(index - pinned_tab_count_);
      SetIdealBoundsAt(index, x_ + stacked_padding);
      LayoutByTabOffsetAfter(index);
      LayoutByTabOffsetBefore(index);
      index--;
    } while (index >= pinned_tab_count_ && ideal_x(pinned_tab_count_) != x_ &&
             ideal_x(tab_count() - 1) != width_ - size_.width());
  }
  AdjustStackedTabs();
}

void StackedTabStripLayout::MakeVisible(int index) {
  // Currently no need to support tabs openning before |index| visible.
  if (index <= active_index() || !requires_stacking() || !IsStacked(index))
    return;

  int ideal_delta = width_for_count(index - active_index()) - overlap_;
  if (ideal_x(index) - ideal_x(active_index()) == ideal_delta)
    return;

  // First push active index as far to the left as it'll go.
  int active_x = std::max(GetMinX(active_index()),
                          std::min(ideal_x(index) - ideal_delta,
                                   ideal_x(active_index())));
  SetIdealBoundsAt(active_index(), active_x);
  LayoutUsingCurrentBefore(active_index());
  LayoutUsingCurrentAfter(active_index());
  AdjustStackedTabs();
  if (ideal_x(index) - ideal_x(active_index()) == ideal_delta)
    return;

  // If we get here active_index() is left aligned. Push |index| as far to
  // the right as possible.
  int x = std::min(GetMaxX(index), active_x + ideal_delta);
  SetIdealBoundsAt(index, x);
  LayoutByTabOffsetAfter(index);
  for (int next_x = x, i = index - 1; i > active_index(); --i) {
    next_x = std::max(GetMinXCompressed(i), next_x - tab_offset());
    SetIdealBoundsAt(i, next_x);
  }
  LayoutUsingCurrentAfter(active_index());
  AdjustStackedTabs();
}

int StackedTabStripLayout::ConstrainActiveX(int x) const {
  return std::min(GetMaxX(active_index()),
                  std::max(GetMinX(active_index()), x));
}

void StackedTabStripLayout::SetActiveBoundsAndLayoutFromActiveTab() {
  int x = ConstrainActiveX(ideal_x(active_index()));
  SetIdealBoundsAt(active_index(), x);
  LayoutUsingCurrentBefore(active_index());
  LayoutUsingCurrentAfter(active_index());
  AdjustStackedTabs();
}

void StackedTabStripLayout::LayoutByTabOffsetAfter(int index) {
  for (int i = index + 1; i < tab_count(); ++i) {
    int max_x = width_ - size_.width() -
        stacked_padding_for_count(tab_count() - i - 1);
    int x = std::min(max_x,
                     view_model_->ideal_bounds(i - 1).x() + tab_offset());
    SetIdealBoundsAt(i, x);
  }
}

void StackedTabStripLayout::LayoutByTabOffsetBefore(int index) {
  for (int i = index - 1; i >= pinned_tab_count_; --i) {
    int min_x = x_ + stacked_padding_for_count(i - pinned_tab_count_);
    int x = std::max(min_x, ideal_x(i + 1) - (tab_offset()));
    SetIdealBoundsAt(i, x);
  }
}

void StackedTabStripLayout::LayoutUsingCurrentAfter(int index) {
  for (int i = index + 1; i < tab_count(); ++i) {
    int min_x = width_ - width_for_count(tab_count() - i);
    int x = std::max(min_x,
                     std::min(ideal_x(i), ideal_x(i - 1) + tab_offset()));
    x = std::min(GetMaxX(i), x);
    SetIdealBoundsAt(i, x);
  }
}

void StackedTabStripLayout::LayoutUsingCurrentBefore(int index) {
  for (int i = index - 1; i >= pinned_tab_count_; --i) {
    int max_x = x_ + width_for_count(i - pinned_tab_count_);
    if (i > pinned_tab_count_)
      max_x -= overlap_;
    max_x = std::min(max_x, ideal_x(i + 1) - stacked_padding_);
    SetIdealBoundsAt(
        i, std::min(max_x,
                    std::max(ideal_x(i), ideal_x(i + 1) - tab_offset())));
  }
}

void StackedTabStripLayout::PushTabsAfter(int index, int delta) {
  for (int i = index + 1; i < tab_count(); ++i)
    SetIdealBoundsAt(i, std::min(ideal_x(i) + delta, GetMaxDragX(i)));
}

void StackedTabStripLayout::PushTabsBefore(int index, int delta) {
  for (int i = index - 1; i > pinned_tab_count_; --i)
    SetIdealBoundsAt(i, std::max(ideal_x(i) - delta, GetMinDragX(i)));
}

void StackedTabStripLayout::LayoutForDragAfter(int index) {
  for (int i = index + 1; i < tab_count(); ++i) {
    const int min_x = ideal_x(i - 1) + stacked_padding_;
    const int max_x = ideal_x(i - 1) + tab_offset();
    SetIdealBoundsAt(
        i, std::max(min_x, std::min(ideal_x(i), max_x)));
  }
}

void StackedTabStripLayout::LayoutForDragBefore(int index) {
  for (int i = index - 1; i >= pinned_tab_count_; --i) {
    const int max_x = ideal_x(i + 1) - stacked_padding_;
    const int min_x = ideal_x(i + 1) - tab_offset();
    SetIdealBoundsAt(
        i, std::max(min_x, std::min(ideal_x(i), max_x)));
  }

  if (pinned_tab_count_ == 0)
    return;

  // Pull in the pinned tabs.
  const int delta = (pinned_tab_count_ > 1) ? ideal_x(1) - ideal_x(0) : 0;
  for (int i = pinned_tab_count_ - 1; i >= 0; --i) {
    gfx::Rect pinned_bounds(view_model_->ideal_bounds(i));
    if (i == pinned_tab_count_ - 1)
      pinned_bounds.set_x(ideal_x(i + 1) - pinned_tab_to_non_pinned_tab_);
    else
      pinned_bounds.set_x(ideal_x(i + 1) - delta);
    view_model_->set_ideal_bounds(i, pinned_bounds);
  }
}

void StackedTabStripLayout::ExpandTabsBefore(int index, int delta) {
  for (int i = index - 1; i >= pinned_tab_count_ && delta > 0; --i) {
    const int max_x = ideal_x(active_index()) -
        stacked_padding_for_count(active_index() - i);
    int to_resize = std::min(delta, max_x - ideal_x(i));

    if (to_resize <= 0)
      continue;
    SetIdealBoundsAt(i, ideal_x(i) + to_resize);
    delta -= to_resize;
    LayoutForDragBefore(i);
  }
}

void StackedTabStripLayout::ExpandTabsAfter(int index, int delta) {
  if (index == tab_count() - 1)
    return;  // Nothing to expand.

  for (int i = index + 1; i < tab_count() && delta > 0; ++i) {
    const int min_compressed =
        ideal_x(active_index()) + stacked_padding_for_count(i - active_index());
    const int to_resize = std::min(ideal_x(i) - min_compressed, delta);
    if (to_resize <= 0)
      continue;
    SetIdealBoundsAt(i, ideal_x(i) - to_resize);
    delta -= to_resize;
    LayoutForDragAfter(i);
  }
}

void StackedTabStripLayout::AdjustStackedTabs() {
  if (!requires_stacking() || tab_count() <= pinned_tab_count_ + 1)
    return;

  AdjustLeadingStackedTabs();
  AdjustTrailingStackedTabs();
}

void StackedTabStripLayout::AdjustLeadingStackedTabs() {
  int index = pinned_tab_count_ + 1;
  while (index < active_index() &&
         ideal_x(index) - ideal_x(index - 1) <= stacked_padding_ &&
         ideal_x(index) <= x_ + max_stacked_width()) {
    index++;
  }
  if (ideal_x(index) - ideal_x(index - 1) <= stacked_padding_ &&
      ideal_x(index) <= x_ + max_stacked_width()) {
    index++;
  }
  if (index <= pinned_tab_count_ + max_stacked_count_ - 1)
    return;
  int max_stacked = index;
  int x = x_;
  index = pinned_tab_count_;
  for (; index < max_stacked - max_stacked_count_ - 1; ++index)
    SetIdealBoundsAt(index, x);
  for (; index < max_stacked; ++index, x += stacked_padding_)
    SetIdealBoundsAt(index, x);
}

void StackedTabStripLayout::AdjustTrailingStackedTabs() {
  int index = tab_count() - 1;
  int max_stacked_x = width_ - size_.width() - max_stacked_width();
  while (index > active_index() &&
         ideal_x(index) - ideal_x(index - 1) <= stacked_padding_ &&
         ideal_x(index - 1) >= max_stacked_x) {
    index--;
  }
  if (index > active_index() &&
      ideal_x(index) - ideal_x(index - 1) <= stacked_padding_ &&
      ideal_x(index - 1) >= max_stacked_x) {
    index--;
  }
  if (index >= tab_count() - max_stacked_count_)
    return;
  int first_stacked = index;
  int x = width_ - size_.width() -
      std::min(tab_count() - first_stacked, max_stacked_count_) *
      stacked_padding_;
  for (; index < first_stacked + max_stacked_count_;
       ++index, x += stacked_padding_) {
    SetIdealBoundsAt(index, x);
  }
  for (; index < tab_count(); ++index)
    SetIdealBoundsAt(index, x);
}

void StackedTabStripLayout::SetIdealBoundsAt(int index, int x) {
  view_model_->set_ideal_bounds(index, gfx::Rect(gfx::Point(x, 0), size_));
}

int StackedTabStripLayout::GetMinX(int index) const {
  int leading_count = index - pinned_tab_count_;
  int trailing_count = tab_count() - index;
  return std::max(x_ + stacked_padding_for_count(leading_count),
                  width_ - width_for_count(trailing_count));
}

int StackedTabStripLayout::GetMaxX(int index) const {
  int leading_count = index - pinned_tab_count_;
  int trailing_count = tab_count() - index - 1;
  int trailing_offset = stacked_padding_for_count(trailing_count);
  int leading_size = width_for_count(leading_count) + x_;
  if (leading_count > 0)
    leading_size -= overlap_;
  return std::min(width_ - trailing_offset - size_.width(), leading_size);
}

int StackedTabStripLayout::GetMinDragX(int index) const {
  return x_ + stacked_padding_for_count(index - pinned_tab_count_);
}

int StackedTabStripLayout::GetMaxDragX(int index) const {
  const int trailing_offset =
      stacked_padding_for_count(tab_count() - index - 1);
  return width_ - trailing_offset - size_.width();
}

int StackedTabStripLayout::GetMinXCompressed(int index) const {
  DCHECK_GT(index, active_index());
  return std::max(
      width_ - width_for_count(tab_count() - index),
      ideal_x(active_index()) +
          stacked_padding_for_count(index - active_index()));
}