#include "roo_windows/containers/scrollable_panel.h"

#include <Arduino.h>

#include "roo_windows/core/application.h"
#include "roo_windows/core/gesture_detector.h"
#include "roo_windows/core/main_window.h"

namespace roo_windows {

namespace {

static const float kDecceleration = 300.0;
static const float kMaxVel = 5000.0;
static const roo_time::Duration kDelayHideScrollbar = roo_time::Millis(1200);

// The area on the side of the panel whose touch is interpreted as an
// interaction with the scroll bar.
static const XDim kScrollBarTouchWidth = 50;

// Scroll bar height is scaled on the basis of much much content there is to
// scroll, but it will never be smaller than this number of pixels.
static const YDim kScrollBarMinHeightPx = 20;

}  // namespace

void VerticalScrollBar::setRange(int16_t begin, int16_t end) {
  if (begin == begin_ && end == end_) return;
  begin_ = begin;
  end_ = end;
  setDirty();
}

void VerticalScrollBar::paint(const Canvas& canvas) const {
  Color s = theme().color.onSurface;
  s.set_a(0xC0);
  s = AlphaBlend(canvas.bgcolor(), s);
  Color semi = theme().color.onSurface;
  semi.set_a(0x40);
  semi = AlphaBlend(canvas.bgcolor(), semi);
  if (begin_ > 0) {
    canvas.fillRect(0, 0, width() - 1, begin_ - 1, semi);
  }
  canvas.fillRect(0, begin_, width() - 1, end_, s);
  if (end_ + 1 < height()) {
    canvas.fillRect(0, end_ + 1, width() - 1, height() - 1, semi);
  }
}

YDim VerticalScrollBar::toOffset(YDim h) const {
  if (h <= height()) {
    // Nothing to scroll.
    return 0;
  }
  // The difference in pixels between the minimum and maximum position of the
  // scroll bar (i.e., the possible range of begin_).
  YDim scroll_range = height() - (end_ - begin_ + 1);
  // The difference in pixels between the minimum and maximum position of the
  // scrolled content (i.e., the possible range of the offset).
  YDim view_range = height() - h;
  return -begin_ * view_range / scroll_range;
}

PreferredSize VerticalScrollBar::getPreferredSize() const {
  return PreferredSize(PreferredSize::ExactWidth(Scaled(6)),
                       PreferredSize::MatchParentHeight());
}

Dimensions VerticalScrollBar::getSuggestedMinimumDimensions() const {
  return Dimensions(Scaled(6), Scaled(6));
}

void ScrollablePanel::scrollTo(XDim x, YDim y) {
  Widget* c = contents();
  if (c == nullptr) return;
  Margins m = c->getMargins();
  Rect inner_pane(m.left(), m.top(), width() - m.right() - 1,
                  height() - m.bottom() - 1);
  auto offset = ResolveAlignmentOffset(bounds(), c->bounds(), alignment_);
  if (c->width() >= inner_pane.width()) {
    if (x > 0) x = 0;
    if (x < inner_pane.width() - c->width()) {
      x = inner_pane.width() - c->width();
    }
  } else {
    // if (x < 0) x = 0;
    // if (x > width() - c->width()) x = width() - c->width();
    x = offset.first;
  }
  if (c->height() >= inner_pane.height()) {
    if (y > 0) y = 0;
    if (y < inner_pane.height() - c->height()) {
      y = inner_pane.height() - c->height();
    }
  } else {
    // if (y < 0) y = 0;
    // if (y > height() - c->height()) y = height() - c->height();
    y = offset.second;
  }
  if (c->height() <= height()) {
    scroll_bar_.setRange(0, height() - 1);
  } else {
    YDim scroll_pix_height =
        std::max(kScrollBarMinHeightPx, height() * height() / c->height());
    YDim scroll_pix_range = height() - scroll_pix_height;
    YDim scroll_pix_begin =
        -(scroll_pix_range) * (y + m.top()) / (c->height() - height());
    scroll_bar_.setRange(scroll_pix_begin,
                         scroll_pix_begin + scroll_pix_height - 1);
  }
  c->moveTo(c->bounds().translate(x + m.left(), y + m.top()));
  onScrollPositionChanged();
}

void ScrollablePanel::scrollToBottom() {
  if (contents() == nullptr) return;
  getMainWindow()->updateLayout();
  Margins m = contents()->getMargins();
  scrollTo(contents()->offsetLeft(),
           height() - m.top() - m.bottom() - contents()->height());
}

PreferredSize ScrollablePanel::getPreferredSize() const {
  // In the dimension that is scrolled over, we will just return 'wrap
  // contents', For example, if the panel scrolls vertically, we report the
  // preferred height as 'wrap contents height'. In the other dimension, we
  // forward the preference of the contents, accounting for our padding and the
  // content's margins (in case when the preference is exact).
  Padding p = getPadding();
  XDim ph = p.left() + p.right();
  YDim pv = p.top() + p.bottom();
  PreferredSize::Width w = PreferredSize::ExactWidth(ph);
  PreferredSize::Height h = PreferredSize::ExactHeight(pv);
  if (contents() != nullptr) {
    PreferredSize c = contents()->getPreferredSize();
    Margins m = contents()->getMargins();
    XDim mh = m.left() + m.right();
    YDim mv = m.top() + m.bottom();
    if (c.width().isExact()) {
      w = PreferredSize::ExactWidth(c.width().value() + ph + mh);
    } else {
      w = c.width();
    }
    if (c.height().isExact()) {
      h = PreferredSize::ExactHeight(c.height().value() + pv + mv);
    } else {
      h = c.height();
    }
  }
  return PreferredSize(
      direction_ == VERTICAL ? w : PreferredSize::WrapContentWidth(),
      direction_ == HORIZONTAL ? h : PreferredSize::WrapContentHeight());
}

Dimensions ScrollablePanel::onMeasure(WidthSpec width, HeightSpec height) {
  if (contents() == nullptr) {
    return Dimensions(width.resolveSize(0), height.resolveSize(0));
  }
  Margins m = contents()->getMargins();
  PreferredSize s = contents()->getPreferredSize();
  WidthSpec child_width =
      (direction_ == VERTICAL)
          ? width.getChildWidthSpec(m.left() + m.right(), s.width())
          : WidthSpec::Unspecified(
                std::max(0, width.value() - m.left() - m.right()));
  HeightSpec child_height =
      (direction_ == HORIZONTAL)
          ? height.getChildHeightSpec(m.top() + m.bottom(), s.height())
          : HeightSpec::Unspecified(
                std::max((YDim)0, height.value() - m.top() - m.bottom()));
  measured_ = contents()->measure(child_width, child_height);
  scroll_bar_.measure(width, height);
  return Dimensions(
      width.resolveSize(measured_.width() + m.left() + m.right()),
      height.resolveSize(measured_.height() + m.top() + m.bottom()));
}

void ScrollablePanel::onLayout(bool changed, const Rect& rect) {
  Widget* c = contents();
  if (c == nullptr) return;
  Margins m = contents()->getMargins();
  Rect bounds(0, 0, measured_.width() - 1, measured_.height() - 1);
  bounds =
      bounds.translate(c->offsetLeft() + m.left(), c->offsetTop() + m.top());
  c->layout(bounds);
  scroll_bar_.layout(
      Rect(rect.width() - Scaled(6), 0, rect.width() - 1, rect.height() - 1));
  update();
}

void ScrollablePanel::execute(roo_scheduler::EventID id) {
  notification_id_ = -1;
  if (roo_time::Uptime::Now() >= deadline_hide_scrollbar_) {
    scroll_bar_.setVisibility(INVISIBLE);
  }
  bool scroll_in_progress = (contents() != nullptr &&
                             (scroll_start_vx_ != 0 || scroll_start_vy_ != 0));
  if (!scroll_in_progress) return;

  Widget* c = contents();
  int16_t drag_x_delta = 0;
  int16_t drag_y_delta = 0;

  // Calculate the total offset to be moved.
  unsigned long t_end = millis();
  if ((long)(t_end - scroll_end_time_) >= 0) {
    // The time to full stop elapsed; the scroll is certainly over now.
    t_end = scroll_end_time_;
    scroll_in_progress = false;
  }
  // Now, a little bit of physics. We calculate total distance traveled
  // by the scrolled content, as S = vt * at^2 (where a < 0), seperately
  // for the horizontal and vertical components.
  // Note that when the scroll gets abruptly terminated in either
  // horizontal or vertical direction because of bumping into a boundary,
  // the corresponding start velocity is set to zero.
  float t = (t_end - scroll_start_time_) / 1000.0;
  if (scroll_start_vx_ != 0) {
    // The scrolling continues horizontally.
    int32_t scroll_x_total =
        (int32_t)(scroll_start_vx_ * t + scroll_decel_x_ * t * t / 2);
    drag_x_delta = scroll_x_total - (c->offsetLeft() - dxStart_);
    // Don't move outside the boundary.
    if (drag_x_delta < -c->offsetLeft() + width() - c->width()) {
      drag_x_delta = -c->offsetLeft() + width() - c->width();
      scroll_start_vx_ = 0;
    }
    if (drag_x_delta > -c->offsetLeft()) {
      drag_x_delta = -c->offsetLeft();
      scroll_start_vx_ = 0;
    }
  }
  if (scroll_start_vy_ != 0) {
    // The scrolling continues vertically.
    int32_t scroll_y_total =
        (int32_t)(scroll_start_vy_ * t + scroll_decel_y_ * t * t / 2);
    drag_y_delta = scroll_y_total - (c->offsetTop() - dyStart_);
    // Don't move outside the boundary.
    if (drag_y_delta < -c->offsetTop() + height() - c->height()) {
      drag_y_delta = -c->offsetTop() + height() - c->height();
      scroll_start_vy_ = 0;
    }
    if (drag_y_delta > -c->offsetTop()) {
      drag_y_delta = -c->offsetTop();
      scroll_start_vy_ = 0;
    }
  }
  if (drag_x_delta != 0 || drag_y_delta != 0) {
    scrollBy(drag_x_delta, drag_y_delta);
    invalidateInterior();
  }

  // If we're done, e.g. due to bumping against both horizontal and
  // vertical boundaries, then stop refreshing the view.
  if (scroll_start_vx_ == 0 && scroll_start_vy_ == 0) {
    scroll_in_progress = false;
  }
  if (scroll_in_progress) {
    scheduleScrollAnimationUpdate();
  } else {
    scroll_start_vx_ = scroll_start_vy_ = 0;
    if (scroll_bar_presence_ == VerticalScrollBar::SHOWN_WHEN_SCROLLING) {
      // Schedule hiding the scroll bar.
      deadline_hide_scrollbar_ = roo_time::Uptime::Now() + kDelayHideScrollbar;
      scheduleHideScrollBarUpdate();
    }
  }
}

void ScrollablePanel::cancelPendingUpdate() {
  if (notification_id_ > 0) {
    scheduler_.cancel(notification_id_);
    notification_id_ = -1;
  }
}

void ScrollablePanel::scheduleScrollAnimationUpdate() {
  cancelPendingUpdate();
  notification_id_ = scheduler_.scheduleAfter(roo_time::Millis(10), *this);
}

void ScrollablePanel::scheduleHideScrollBarUpdate() {
  cancelPendingUpdate();
  scheduler_.scheduleAfter(kDelayHideScrollbar, *this);
}

bool ScrollablePanel::onInterceptTouchEvent(const TouchEvent& event) {
  if (scroll_start_vx_ != 0 || scroll_start_vy_ != 0) {
    // Scroll in progress. Capture all events, including touch down.
    return true;
  }
  if (scroll_bar_presence_ != VerticalScrollBar::ALWAYS_HIDDEN &&
      contents() != nullptr && contents()->height() > height() &&
      event.x() >= width() - kScrollBarTouchWidth) {
    scroll_bar_gesture_ = true;
    // We set a higher bar for recognizing the interaction as an actual touch of
    // the scroll bar (leading to a scroll event on the scroll bar): the touch
    // must happen in the right place. (We still allow the scroll bar to not be
    // visible at the moment).
    is_scroll_bar_scrolled_ =
        (event.y() >= scroll_bar_.begin() && event.y() <= scroll_bar_.end());
    return is_scroll_bar_scrolled_ || scroll_bar_.isVisible();
  }
  // Don't intercept too aggressively, as it prevents the child to handle its
  // own scrolling (which may be in a different direction.) Instead, rely on the
  // child to relinquish control by returning false from onMove.
  // if (event.type() == TouchEvent::MOVE) {
  //   // If we detect drag motion in the scroll direction, intercept, and turn
  //   // into a scroll event.
  //   const GestureDetector& gd = getApplication()->gesture_detector();
  //   int16_t dx = (direction_ == VERTICAL) ? 0 : gd.xTotalMoveDelta();
  //   int16_t dy = (direction_ == HORIZONTAL) ? 0 : gd.yTotalMoveDelta();
  //   if (dx * dx + dy * dy > kTouchSlopSquare) {
  //     return true;
  //   }
  // }
  return false;
}

bool ScrollablePanel::onDown(XDim x, YDim y) {
  // Stop the scroll.
  scroll_start_vx_ = 0;
  scroll_start_vy_ = 0;
  return true;
}

bool ScrollablePanel::onSingleTapUp(XDim x, YDim y) {
  // We generally swallow and ignore tap events, except when tapping on
  // a scrollbar area.
  if (!scroll_bar_gesture_) return true;
  if (scroll_bar_.isVisible()) {
    // Interpret as an immediate jump.
    // Calculate the difference in pixels between the minimum and maximum
    // position of the scroll bar (i.e., the possible range of begin_).
    YDim scroll_range =
        height() - (scroll_bar_.end() - scroll_bar_.begin() + 1);
    // Calculate the difference in pixels between the minimum and maximum
    // position of the scrolled content (i.e., the possible range of the
    // offset).
    YDim view_range = contents()->height() - height();
    if (scroll_range > 0) {
      YDim new_y = -(y - (scroll_bar_.end() - scroll_bar_.begin()) / 2) *
                   view_range / scroll_range;
      // The new y might be out of range, but that's ok - scrollTo will trim it.
      scrollTo(contents()->offsetLeft(), new_y);
    }
  } else {
    scroll_bar_.setVisibility(VISIBLE);
  }
  deadline_hide_scrollbar_ = roo_time::Uptime::Now() + kDelayHideScrollbar;
  scheduleHideScrollBarUpdate();
  return true;
}

bool ScrollablePanel::onScroll(XDim x, YDim y, XDim dx, YDim dy) {
  if (contents() == nullptr || contents()->height() <= height()) {
    // Nothing to scroll.
    return false;
  }
  if (scroll_bar_gesture_) {
    if (is_scroll_bar_scrolled_) {
      // Calculate the difference in pixels between the minimum and maximum
      // position of the scroll bar (i.e., the possible range of begin_).
      YDim scroll_range =
          height() - (scroll_bar_.end() - scroll_bar_.begin() + 1);
      // Calculate the difference in pixels between the minimum and maximum
      // position of the scrolled content (i.e., the possible range of the
      // offset).
      YDim view_range = contents()->height() - height();
      YDim y_shift = -dy * view_range / scroll_range;
      // The new y might be out of range, but that's ok - scrollBy will trim it.
      scrollBy(contents()->offsetLeft(), y_shift);
    }
  } else {
    scrollBy(dx, dy);
  }
  if (scroll_bar_presence_ == VerticalScrollBar::SHOWN_WHEN_SCROLLING) {
    scroll_bar_.setVisibility(VISIBLE);
    deadline_hide_scrollbar_ = roo_time::Uptime::Now() + roo_time::Hours(1);
  }
  return true;
}

bool ScrollablePanel::onFling(XDim x, YDim y, XDim vx, YDim vy) {
  if (contents() == nullptr) return true;
  if (scroll_bar_gesture_) {
    // No fling-animate on the scrollbar.
    return true;
  }
  Widget* c = contents();
  scroll_start_time_ = millis();
  scroll_start_vx_ = vx;
  scroll_start_vy_ = vy;
  dxStart_ = c->offsetLeft();
  dyStart_ = c->offsetTop();
  // If we're already on the boundary and swiping outside of the bounded
  // region, cap the horizontal and/or vertical component of the scroll.
  if (scroll_start_vx_ < 0 && c->offsetLeft() == 0) {
    scroll_start_vx_ = 0;
  }
  if (scroll_start_vx_ > 0 && -c->offsetLeft() + width() == c->width()) {
    scroll_start_vx_ = 0;
  }
  if (scroll_start_vy_ < 0 && c->offsetTop() == 0) {
    scroll_start_vy_ = 0;
  }
  if (scroll_start_vy_ > 0 && -c->offsetTop() + height() == c->height()) {
    scroll_start_vy_ = 0;
  }
  // If the swipe is completely in the outside direction, ignore it.
  if (scroll_start_vx_ == 0 && scroll_start_vy_ == 0) return true;
  // Calculate the length of the initial velocity vector.
  float v_abs = sqrtf(scroll_start_vx_ * scroll_start_vx_ +
                      scroll_start_vy_ * scroll_start_vy_);
  // Cap that scroll velocity if too large.
  if (v_abs > kMaxVel) {
    float mult = kMaxVel / v_abs;
    scroll_start_vx_ *= mult;
    scroll_start_vy_ *= mult;
    v_abs = kMaxVel;
  }
  // Scroll is constantly deccelerating (with configured const
  // decceleration). Calculate the total scroll time until it stops on
  // its own if uninterrupted.
  unsigned long scroll_duration =
      (unsigned long)(1000.0 * v_abs / kDecceleration);
  scroll_end_time_ = scroll_start_time_ + scroll_duration;
  // Capture horizontal and vertical components of the decceleration.
  scroll_decel_x_ = -kDecceleration * scroll_start_vx_ / v_abs;
  scroll_decel_y_ = -kDecceleration * scroll_start_vy_ / v_abs;
  scheduleScrollAnimationUpdate();
  return true;
}

bool ScrollablePanel::onTouchUp(XDim vx, YDim vy) {
  bool result = Widget::onTouchUp(vx, vy);
  scroll_bar_gesture_ = false;
  is_scroll_bar_scrolled_ = false;
  if (scroll_bar_presence_ == VerticalScrollBar::SHOWN_WHEN_SCROLLING &&
      scroll_start_vx_ == 0 && scroll_start_vy_ == 0) {
    deadline_hide_scrollbar_ = roo_time::Uptime::Now() + kDelayHideScrollbar;
    scheduleHideScrollBarUpdate();
  }
  return result;
}

}  // namespace roo_windows
