// Released under the MIT License. See LICENSE for details.

#include "ballistica/ui/widget/scroll_widget.h"

#include "ballistica/generic/real_timer.h"
#include "ballistica/graphics/component/empty_component.h"
#include "ballistica/graphics/component/simple_component.h"
#include "ballistica/ui/ui.h"

namespace ballistica {

#define V_MARGIN 5

ScrollWidget::ScrollWidget() : touch_mode_(!g_platform->IsRunningOnDesktop()) {
  set_background(false);  // Influences default event handling.
  set_draggable(false);
  set_claims_left_right(false);
  set_claims_tab(false);
}

ScrollWidget::~ScrollWidget() = default;

void ScrollWidget::HandleRealTimerExpired(RealTimer<ScrollWidget>* t) {
  if (touch_held_) {
    // Pass a mouse-down event if we haven't moved.
    if (!touch_is_scrolling_ && !touch_down_sent_) {
      ContainerWidget::HandleMessage(
          WidgetMessage(WidgetMessage::Type::kMouseDown, nullptr, touch_x_,
                        touch_y_, static_cast<float>(touch_held_click_count_)));
      touch_down_sent_ = true;
    }
  }

  // Clean ourself out.
  touch_delay_timer_.Clear();
}

void ScrollWidget::ClampThumb(bool velocity_clamp, bool position_clamp) {
  BA_DEBUG_UI_READ_LOCK;

  bool is_scrolling;
  if (touch_mode_) {
    is_scrolling = (touch_held_ || !has_momentum_);
  } else {
    is_scrolling = (!has_momentum_);
  }
  float strong_force;
  float weak_force;
  if (touch_mode_) {
    strong_force = -0.12f;
    weak_force = -0.004f;
  } else {
    strong_force = -0.012f;
    weak_force = -0.004f;
  }
  auto i = widgets().begin();
  if (i != widgets().end()) {
    float child_h = (**i).GetHeight();
    if (velocity_clamp) {
      if (child_offset_v_ < 0) {
        // Even in velocity case do some sane clamping.
        float diff = child_offset_v_;
        inertia_scroll_rate_ +=
            diff * (is_scrolling ? strong_force : weak_force);
        inertia_scroll_rate_ *= 0.9f;

      } else if (child_offset_v_
                 > child_h - (height() - 2 * (border_height_ + V_MARGIN))) {
        float diff =
            child_offset_v_
            - (child_h
               - std::min(child_h,
                          (height() - 2 * (border_height_ + V_MARGIN))));
        inertia_scroll_rate_ +=
            diff * (is_scrolling ? strong_force : weak_force);
        inertia_scroll_rate_ *= 0.9f;
      }
    }

    // Hard clipping if we're dragging the scrollbar.
    if (position_clamp) {
      if (child_offset_v_smoothed_
          > child_h - (height() - 2 * (border_height_ + V_MARGIN))) {
        child_offset_v_smoothed_ =
            child_h - (height() - 2 * (border_height_ + V_MARGIN));
      }
      if (child_offset_v_smoothed_ < 0) {
        child_offset_v_smoothed_ = 0;
      }
      if (child_offset_v_
          > child_h - (height() - 2 * (border_height_ + V_MARGIN))) {
        child_offset_v_ =
            child_h - (height() - 2 * (border_height_ + V_MARGIN));
      }
      if (child_offset_v_ < 0) {
        child_offset_v_ = 0;
      }
    }
  }
}

auto ScrollWidget::HandleMessage(const WidgetMessage& m) -> bool {
  BA_DEBUG_UI_READ_LOCK;
  bool claimed = false;
  bool pass = true;
  float right_overlap = 0;
  float left_overlap = 3;
  switch (m.type) {
    case WidgetMessage::Type::kMoveUp:
      if (capture_arrows_) {
        smoothing_amount_ = 1.0f;  // So we can see the transition.
        child_offset_v_ -= (60);
        MarkForUpdate();
        ClampThumb(false, true);
      }
      break;

    case WidgetMessage::Type::kMoveDown:
      if (capture_arrows_) {
        smoothing_amount_ = 1.0f;  // So we can see the transition.
        child_offset_v_ += (60);
        MarkForUpdate();
        ClampThumb(false, true);
      }
      break;

    case WidgetMessage::Type::kShow: {
      claimed = true;
      pass = false;
      auto i = widgets().begin();
      if (i == widgets().end()) break;
      float child_h = (**i).GetHeight();

      // See where we'd have to scroll to get selection at top and bottom.
      float child_offset_bot =
          child_h - m.fval2 - (height() - 2 * (border_height_ + V_MARGIN));
      float child_offset_top = child_h - m.fval2 - m.fval4;

      // If we're in the middle, dont do anything.
      if (child_offset_v_ > child_offset_bot
          && child_offset_v_ < child_offset_top) {
      } else {
        // Do whatever offset is less of a move.
        if (std::abs(child_offset_bot - child_offset_v_)
            < std::abs(child_offset_top - child_offset_v_)) {
          child_offset_v_ = child_offset_bot;
        } else {
          child_offset_v_ = child_offset_top;
        }

        // If we're moving down, stop at the bottom.
        {
          float max_val =
              child_h - (height() - 2 * (border_height_ + V_MARGIN));
          if (child_offset_v_ > max_val) {
            child_offset_v_ = max_val;
          }
        }

        // If we're moving up, stop at the top.
        {
          if (child_offset_v_ < 0) {
            child_offset_v_ = 0;
          }
        }
      }

      // Go into smooth mode momentarily.
      smoothing_amount_ = 1.0f;

      // Snap our smoothed value to this *only* if we haven't drawn yet.
      // (keeps new widgets from inexplicably scrolling around)
      if (!have_drawn_) {
        child_offset_v_smoothed_ = child_offset_v_;
      }
      MarkForUpdate();
      break;
    }
    case WidgetMessage::Type::kMouseWheelVelocityH: {
      if (ContainerWidget::HandleMessage(m)) {
        claimed = true;

        // Keep track of the average scrolling going on. (only update when we
        // get non-momentum events)
        if (std::abs(m.fval3) > 0.001f && !has_momentum_) {
          float smoothing = 0.8f;
          avg_scroll_speed_h_ =
              smoothing * avg_scroll_speed_h_ + (1.0f - smoothing) * m.fval3;

          // Also tamp this down in case we're not getting new events for it.
          avg_scroll_speed_v_ =
              smoothing * avg_scroll_speed_v_ + (1.0f - smoothing) * 0.0f;
        }
        last_sub_widget_h_scroll_claim_time_ = GetRealTime();
      }
      pass = false;
      break;
    }
    case WidgetMessage::Type::kMouseWheelVelocity: {
      float x = m.fval1;
      float y = m.fval2;

      // Keep track of the average scrolling going on. (only update when we get
      // non-momentum events).
      if (std::abs(m.fval3) > 0.001f && !has_momentum_) {
        float smoothing = 0.8f;
        avg_scroll_speed_v_ =
            smoothing * avg_scroll_speed_v_ + (1.0f - smoothing) * m.fval3;

        // Also tamp this down in case we're not getting new events for it.
        avg_scroll_speed_h_ =
            smoothing * avg_scroll_speed_h_ + (1.0f - smoothing) * 0.0f;
      }

      // If a child appears to be looking at horizontal scroll events and we're
      // scrolling more horizontally than vertically in general, ignore vertical
      // scrolling (should probably make this less fuzzy).
      bool ignore_regular_scrolling = false;
      bool child_claimed_h_scroll_recently =
          (GetRealTime() - last_sub_widget_h_scroll_claim_time_ < 100);
      if (child_claimed_h_scroll_recently
          && std::abs(avg_scroll_speed_h_) > std::abs(avg_scroll_speed_v_))
        ignore_regular_scrolling = true;

      if ((x >= 0.0f) && (x < width()) && (y >= 0.0f) && (y < height())
          && !ignore_regular_scrolling) {
        claimed = true;
        pass = false;
        has_momentum_ = static_cast<bool>(m.fval4);

        // We only set velocity from events when not in momentum mode. We
        // handle momentum ourself.
        if (std::abs(m.fval3) > 0.001f && !has_momentum_) {
          float scroll_speed = 2.2f;
          float smoothing = 0.8f;
          float new_val;
          if (m.fval3 < 0.0f) {
            // Apply less if we're past the end.
            if (child_offset_v_ < 0) {
              new_val = scroll_speed * 0.1f * m.fval3;
            } else {
              new_val = scroll_speed * m.fval3;
            }
          } else {
            // Apply less if we're past the end.
            bool past_end = false;

            // Calc our total height.
            auto i = widgets().begin();
            if (i != widgets().end()) {
              float child_h = (**i).GetHeight();
              float diff =
                  child_offset_v_
                  - (child_h
                     - std::min(child_h,
                                (height() - 2 * (border_height_ + V_MARGIN))));
              if (diff > 0) past_end = true;
            }
            if (past_end) {
              new_val = scroll_speed * 0.1f * m.fval3;
            } else {
              new_val = scroll_speed * m.fval3;
            }
          }
          inertia_scroll_rate_ =
              smoothing * inertia_scroll_rate_ + (1.0f - smoothing) * new_val;
        }
        last_velocity_event_time_ = g_game->master_time();
        MarkForUpdate();
      } else {
        // Not within our widget; don't allow children to claim.
        pass = false;
      }
      break;
    }
    case WidgetMessage::Type::kMouseWheel: {
      float x = m.fval1;
      float y = m.fval2;
      if ((x >= 0.0f) && (x < width()) && (y >= 0.0f) && (y < height())) {
        claimed = true;
        pass = false;
        inertia_scroll_rate_ -= m.fval3 * 0.003f;
        MarkForUpdate();
      } else {
        // Not within our widget; dont allow children to claim.
        pass = false;
      }
      break;
    }
    case WidgetMessage::Type::kMouseDown: {
      float x = m.fval1;
      float y = m.fval2;
      if ((x >= 0.0f) && (x < width() + right_overlap) && (y >= 0.0f)
          && (y < height())) {
        // On touch devices, touches begin scrolling, (and eventually can count
        // as clicks if they don't move).
        if (touch_mode_) {
          touch_held_ = true;
          auto click_count = static_cast<int>(m.fval3);
          touch_held_click_count_ = click_count;
          touch_down_sent_ = false;
          touch_up_sent_ = false;
          touch_start_x_ = x;
          touch_start_y_ = y;
          touch_x_ = x;
          touch_y_ = y;
          touch_down_y_ = y - child_offset_v_;
          touch_is_scrolling_ = false;
          child_is_scrolling_ = false;
          child_disowned_scroll_ = false;

          // If there's already significant scrolling happening, we handle all
          // these ourself as scroll events.
          if (std::abs(inertia_scroll_rate_) > 0.05f) {
            touch_is_scrolling_ = true;
          }

          pass = false;
          claimed = true;

          if (!touch_is_scrolling_) {
            // Give children a chance to claim this for their own scrolling
            // before we do so.
            child_is_scrolling_ = ContainerWidget::HandleMessage(
                WidgetMessage(WidgetMessage::Type::kScrollMouseDown, nullptr,
                              m.fval1, m.fval2, m.fval3));

            // After a short delay we go ahead and handle this as a regular
            // click if it hasn't turned into a scroll or a child scroll.
            if (!child_is_scrolling_) {
              touch_delay_timer_ =
                  Object::New<RealTimer<ScrollWidget>>(150, false, this);
            }
          }
        }

        // On desktop, allow clicking on the scrollbar.
        if (!touch_mode_) {
          if (x >= width() - scroll_bar_width_ - left_overlap) {
            claimed = true;
            pass = false;
            float s_top = height() - border_height_;
            float s_bottom = border_height_;
            float sb_thumb_height =
                amount_visible_ * (height() - 2 * border_height_);
            float sb_thumb_top = s_top
                                 - child_offset_v_ / child_max_offset_
                                       * (s_top - (s_bottom + sb_thumb_height));
            // Above thumb (page-up).
            if (y >= sb_thumb_top) {
              // So we can see the transition.
              smoothing_amount_ = 1.0f;
              child_offset_v_ -= (height() - 2 * (border_height_ + V_MARGIN));
              MarkForUpdate();
              ClampThumb(false, true);
            } else if (y >= sb_thumb_top - sb_thumb_height) {
              // On thumb.
              mouse_held_thumb_ = true;
              thumb_click_start_v_ = y;
              thumb_click_start_child_offset_v_ = child_offset_v_;
            } else if (y >= s_bottom) {
              // Below thumb (page down).
              // So we can see the transition.
              smoothing_amount_ = 1.0f;
              child_offset_v_ += (height() - 2 * (border_height_ + V_MARGIN));
              MarkForUpdate();
              ClampThumb(false, true);
            }
          }
        }
      } else {
        // Not in the scroll box; dont allow children to claim it.
        pass = false;
      }
      break;
    }
    case WidgetMessage::Type::kMouseMove: {
      float x = m.fval1;
      float y = m.fval2;
      bool was_claimed = (m.fval3 > 0.0f);

      // If coords are outside of our bounds we don't want to pass mouse-moved
      // events through the standard container logic. (otherwise, if we mouse
      // down over a button that doesn't overlap the scroll area but overlaps
      // some widget in the scroll area, the widget would claim the move and the
      // button would lose its mouse-over-highlight; ew.) There may be some
      // case where we *would* want to pass this though.
      if (!((x >= 0.0f) && (x < width() + right_overlap) && (y >= 0.0f)
            && (y < height()))) {
        pass = false;
      }

      if (was_claimed) {
        mouse_over_thumb_ = false;
      } else {
        if (touch_mode_) {
          if (touch_held_) {
            // If we have a child claiming this scrolling action for themselves,
            // just keep passing them the events as long as they get claimed.
            if (child_is_scrolling_ && !child_disowned_scroll_) {
              bool move_claimed = ContainerWidget::HandleMessage(
                  WidgetMessage(WidgetMessage::Type::kMouseMove, nullptr,
                                m.fval1, m.fval2, m.fval3));
              // If they stopped claiming them, send a scroll-mouse-up to tie
              // things up.
              if (!move_claimed) {
                ContainerWidget::HandleMessage(
                    WidgetMessage(WidgetMessage::Type::kMouseUp, nullptr,
                                  m.fval1, m.fval2, true));
                child_disowned_scroll_ = true;
              }
            } else {
              // If no child is scrolling; this touch motion is ours to handle.
              touch_x_ = x;
              touch_y_ = y;

              // If we move more than a slight amount it means our touch isn't a
              // click.
              if (!touch_is_scrolling_
                  && ((std::abs(touch_x_ - touch_start_x_) > 10.0f)
                      || (std::abs(touch_y_ - touch_start_y_) > 10.0f))) {
                touch_is_scrolling_ = true;

                // Go ahead and send a mouse-up to the sub-widgets; in their
                // eyes the click is canceled.
                if (touch_down_sent_ && !touch_up_sent_) {
                  ContainerWidget::HandleMessage(
                      WidgetMessage(WidgetMessage::Type::kMouseUp, nullptr,
                                    m.fval1, m.fval2, true));
                  touch_up_sent_ = true;
                }
              }
            }
            return true;
          }
        }

        if (touch_mode_) {
          mouse_over_thumb_ = false;
        } else {
          float s_top = height() - border_height_;
          float s_bottom = border_height_;
          float sb_thumb_height =
              amount_visible_ * (height() - 2.0f * border_height_);
          float sb_thumb_top = s_top
                               - child_offset_v_ / child_max_offset_
                                     * (s_top - (s_bottom + sb_thumb_height));

          mouse_over_thumb_ =
              (((x >= width() - scroll_bar_width_ - left_overlap)
                && (x < width() + right_overlap) && y < sb_thumb_top
                && y >= sb_thumb_top - sb_thumb_height));
        }
      }

      // If we're dragging.
      if (mouse_held_thumb_) {
        auto i = widgets().begin();
        if (i == widgets().end()) {
          break;
        }
        float child_h = (**i).GetHeight();
        float s_top = height() - border_height_;
        float s_bottom = border_height_;
        float rate =
            (child_h - (s_top - s_bottom))
            / ((1.0f - ((s_top - s_bottom) / child_h)) * (s_top - s_bottom));
        child_offset_v_ = thumb_click_start_child_offset_v_
                          - rate * (y - thumb_click_start_v_);
        ClampThumb(false, true);
        MarkForUpdate();
      }
      break;
    }
    case WidgetMessage::Type::kMouseUp: {
      mouse_held_scroll_down_ = false;
      mouse_held_scroll_up_ = false;
      mouse_held_thumb_ = false;
      mouse_held_page_down_ = false;
      mouse_held_page_up_ = false;
      if (touch_mode_) {
        if (touch_held_) {
          touch_held_ = false;

          // If we moved at all, we mark it as claimed to keep
          // sub-widgets from acting on it (since we used it for scrolling)
          bool claimed2 = touch_is_scrolling_ || child_is_scrolling_;

          // if a child is still scrolling, send them a scroll-mouse-up
          if (child_is_scrolling_ && !child_disowned_scroll_) {
            ContainerWidget::HandleMessage(
                WidgetMessage(WidgetMessage::Type::kMouseUp, nullptr, m.fval1,
                              m.fval2, false));
          }

          // If we're not claiming it and we haven't sent a mouse_down yet due
          // to our delay, send that first..
          if (!claimed2 && !touch_down_sent_) {
            ContainerWidget::HandleMessage(WidgetMessage(
                WidgetMessage::Type::kMouseDown, nullptr, m.fval1, m.fval2,
                static_cast<float>(touch_held_click_count_)));
            touch_down_sent_ = true;
          }
          if (touch_down_sent_ && !touch_up_sent_) {
            ContainerWidget::HandleMessage(
                WidgetMessage(WidgetMessage::Type::kMouseUp, nullptr, m.fval1,
                              m.fval2, claimed2));
            touch_up_sent_ = true;
          }

          return true;
        }
      }

      // If coords are outside of our bounds, pass a mouse-up along for anyone
      // tracking a drag, but mark it as claimed so it doesn't actually get
      // acted on.
      float x = m.fval1;
      float y = m.fval2;
      if (!((x >= 0.0f) && (x < width() + right_overlap) && (y >= 0.0f)
            && (y < height()))) {
        pass = false;
        ContainerWidget::HandleMessage(WidgetMessage(
            WidgetMessage::Type::kMouseUp, nullptr, m.fval1, m.fval2, true));
      }

      break;
    }
    default:
      break;
  }

  // Normal container event handling.
  if (pass) {
    if (ContainerWidget::HandleMessage(m)) {
      claimed = true;
    }
  }

  // If it was a mouse-down and we claimed it, set ourself as selected
  if (m.type == WidgetMessage::Type::kMouseDown && claimed) {
    GlobalSelect();
  }

  return claimed;
}

void ScrollWidget::UpdateLayout() {
  BA_DEBUG_UI_READ_LOCK;

  // move everything based on our offset
  auto i = widgets().begin();
  if (i == widgets().end()) {
    amount_visible_ = 0;
    return;
  }
  float child_h = (**i).GetHeight();
  child_max_offset_ = child_h - (height() - 2 * (border_height_ + V_MARGIN));
  amount_visible_ = (height() - 2 * (border_height_ + V_MARGIN)) / child_h;
  if (amount_visible_ > 1) {
    amount_visible_ = 1;
    if (center_small_content_) {
      center_offset_y_ = child_max_offset_ * 0.5f;
    } else {
      center_offset_y_ = 0;
    }
  } else {
    center_offset_y_ = 0;
  }

  if (mouse_held_thumb_) {
    if (child_offset_v_
        > child_h - (height() - 2 * (border_height_ + V_MARGIN))) {
      child_offset_v_ = child_h - (height() - 2 * (border_height_ + V_MARGIN));
      inertia_scroll_rate_ = 0;
    }
    if (child_offset_v_ < 0) {
      child_offset_v_ = 0;
      inertia_scroll_rate_ = 0;
    }
  }
  (**i).set_translate(4 + border_width_, height() - (border_height_ + V_MARGIN)
                                             + child_offset_v_smoothed_
                                             - child_h + center_offset_y_);
  thumb_dirty_ = true;
}

void ScrollWidget::Draw(RenderPass* pass, bool draw_transparent) {
  have_drawn_ = true;
  millisecs_t current_time = pass->frame_def()->base_time();
  float prev_child_offset_v_smoothed = child_offset_v_smoothed_;

  // ok lets update our inertial scrolling during the opaque pass
  // (we really should have some sort of update() function for this but widgets
  // don't have that)
  if (!draw_transparent) {
    // (skip huge differences)
    if (current_time - inertia_scroll_update_time_ > 1000) {
      inertia_scroll_update_time_ = current_time - 1000;
    }
    while (current_time - inertia_scroll_update_time_ > 5) {
      inertia_scroll_update_time_ += 5;

      if (touch_mode_) {
        if (touch_held_) {
          float diff = (touch_y_ - child_offset_v_) - touch_down_y_;
          float smoothing = 0.7f;
          inertia_scroll_rate_ = smoothing * inertia_scroll_rate_
                                 + (1.0f - smoothing) * 0.2f * diff;
        } else {
          inertia_scroll_rate_ *= 0.98f;
        }
      } else {
        inertia_scroll_rate_ *= 0.98f;
      }
      ClampThumb(true, mouse_held_thumb_);
      child_offset_v_ += inertia_scroll_rate_;
      if (!has_momentum_
          && (current_time - last_velocity_event_time_ > 1000 / 30))
        inertia_scroll_rate_ = 0;

      // lastly we apply smoothing so that if we're snapping to a specific place
      // we don't go instantly there we blend between smoothed and non-smoothed
      // depending on whats driving us (we dont want to add smoothing on top of
      // inertial scrolling for example or it'll feel muddy)
      float diff = child_offset_v_ - child_offset_v_smoothed_;
      if (std::abs(diff) < 1.0f) {
        child_offset_v_smoothed_ = child_offset_v_;
      } else {
        child_offset_v_smoothed_ += (1.0f - 0.95f * smoothing_amount_) * diff;
      }
      smoothing_amount_ = std::max(0.0f, smoothing_amount_ - 0.005f);
    }
    // only re-layout our widgets if we've moved a significant amount
    if (std::abs(prev_child_offset_v_smoothed - child_offset_v_smoothed_)
        > 0.01f) {
      MarkForUpdate();
    }
  }

  CheckLayout();

  Vector3f tilt = 0.02f * g_graphics->tilt();
  float extra_offs_x = tilt.y;
  float extra_offs_y = -tilt.x;

  float l = 0;
  float b = 0;
  float t = b + height();

  // begin clipping for children
  {
    EmptyComponent c(pass);
    c.SetTransparent(draw_transparent);
    c.ScissorPush(Rect(l + border_width_, b + border_height_ + 1,
                       l + (width() - border_width_ - 0),
                       b + (height() - border_height_) - 1));
    c.Submit();
  }

  set_simple_culling_bottom(b + border_height_ + 1);
  set_simple_culling_top(b + (height() - border_height_) - 1);

  // draw all our widgets at our z level
  DrawChildren(pass, draw_transparent, l + extra_offs_x, b + extra_offs_y,
               1.0f);

  // end clipping...
  {
    EmptyComponent c(pass);
    c.SetTransparent(draw_transparent);
    c.ScissorPop();
    c.Submit();
  }

  // scroll trough (depth 0.7f to 0.8f)
  if (draw_transparent) {
    if (trough_dirty_) {
      float r2 = l + width();
      float l2 = r2 - scroll_bar_width_;
      float b2;
      float t2;
      b2 = b + (border_height_);
      t2 = t - (border_height_);
      float l_border, r_border, b_border, t_border;
      l_border = 3;
      r_border = 0;
      b_border = height() * 0.006f;
      t_border = height() * 0.002f;
      trough_width_ = r2 - l2 + l_border + r_border;
      trough_height_ = t2 - b2 + b_border + t_border;
      trough_center_x_ = l2 - l_border + trough_width_ * 0.5f;
      trough_center_y_ = b2 - b_border + trough_height_ * 0.5f;
      trough_dirty_ = false;
    }
    SimpleComponent c(pass);
    c.SetTransparent(true);
    c.SetColor(1, 1, 1, border_opacity_);
    c.SetTexture(g_media->GetTexture(SystemTextureID::kUIAtlas));
    c.PushTransform();
    c.Translate(trough_center_x_, trough_center_y_, 0.7f);
    c.Scale(trough_width_, trough_height_, 0.1f);
    c.DrawModel(g_media->GetModel(SystemModelID::kScrollBarTroughTransparent));
    c.PopTransform();
    c.Submit();
  }

  // scroll bars
  if (amount_visible_ > 0 && amount_visible_ < 1) {
    // scroll thumb at depth 0.8f-0.9
    {
      float sb_thumb_height = amount_visible_ * (height() - 2 * border_height_);
      if (thumb_dirty_) {
        float sb_thumb_top =
            t - border_height_
            - ((height() - (border_height_ * 2) - sb_thumb_height)
               * child_offset_v_smoothed_ / child_max_offset_);
        float r2 = l + width();
        float l2 = r2 - scroll_bar_width_;
        float t2 = sb_thumb_top;
        float b2 = t2 - sb_thumb_height;
        float l_border, r_border, b_border, t_border;
        l_border = 6;
        r_border = 3;
        if (sb_thumb_height > 100) {
          b_border = (t2 - b2) * 0.06f;
          t_border = b_border * 0.5f;
        } else {
          b_border = (t2 - b2) * 0.12f;
          t_border = b_border * 0.6f;
        }
        thumb_width_ = r2 - l2 + l_border + r_border;
        thumb_height_ = t2 - b2 + b_border + t_border;
        thumb_center_x_ = l2 - l_border + thumb_width_ * 0.5f;
        thumb_center_y_ = b2 - b_border + thumb_height_ * 0.5f;
        thumb_dirty_ = false;
      }

      SimpleComponent c(pass);
      c.SetTransparent(draw_transparent);
      float c_scale = 1.0f;
      if (mouse_held_thumb_) {
        c_scale = 1.8f;
      } else if (mouse_over_thumb_) {
        c_scale = 1.25f;
      }

      c.SetColor(color_red_ * c_scale, color_green_ * c_scale,
                 color_blue_ * c_scale, 1.0f);

      c.SetTexture(g_media->GetTexture(SystemTextureID::kUIAtlas));
      c.ScissorPush(Rect(l + border_width_, b + border_height_ + 1,
                         l + (width()), b + (height() * 0.995f)));
      c.PushTransform();
      c.Translate(thumb_center_x_, thumb_center_y_, 0.8f);
      c.Scale(thumb_width_, thumb_height_, 0.1f);
      if (draw_transparent) {
        c.DrawModel(g_media->GetModel(
            sb_thumb_height > 100
                ? SystemModelID::kScrollBarThumbTransparent
                : SystemModelID::kScrollBarThumbShortTransparent));
      } else {
        c.DrawModel(g_media->GetModel(
            sb_thumb_height > 100 ? SystemModelID::kScrollBarThumbOpaque
                                  : SystemModelID::kScrollBarThumbShortOpaque));
      }
      c.PopTransform();
      c.ScissorPop();
      c.Submit();
    }
  }

  // outline shadow (depth 0.9f to 1.0f)
  if (draw_transparent) {
    if (shadow_dirty_) {
      float r2 = l + width();
      float l2 = l;
      float b2 = b;
      float t2 = t;
      float l_border, r_border, b_border, t_border;
      l_border = (r2 - l2) * 0.01f;
      r_border = (r2 - l2) * 0.01f;
      b_border = (t2 - b2) * 0.003f;
      t_border = (t2 - b2) * 0.002f;
      outline_width_ = r2 - l2 + l_border + r_border;
      outline_height_ = t2 - b2 + b_border + t_border;
      outline_center_x_ = l2 - l_border + 0.5f * outline_width_;
      outline_center_y_ = b2 - b_border + 0.5f * outline_height_;
      shadow_dirty_ = false;
    }
    SimpleComponent c(pass);
    c.SetTransparent(true);
    c.SetColor(1, 1, 1, border_opacity_);
    c.SetTexture(g_media->GetTexture(SystemTextureID::kScrollWidget));
    c.PushTransform();
    c.Translate(outline_center_x_, outline_center_y_, 0.9f);
    c.Scale(outline_width_, outline_height_, 0.1f);
    c.DrawModel(g_media->GetModel(SystemModelID::kSoftEdgeOutside));
    c.PopTransform();
    c.Submit();
  }

  // if selected, do glow at depth 0.9f-1.0
  if (draw_transparent && IsHierarchySelected()
      && g_ui->ShouldHighlightWidgets() && highlight_) {
    float m = 0.8f
              + std::abs(sinf(static_cast<float>(current_time) * 0.006467f))
                    * 0.2f * border_opacity_;
    if (glow_dirty_) {
      float r2 = l + width();
      float l2 = l;
      float b2 = b;
      float t2 = t;
      float l_border, r_border, b_border, t_border;
      l_border = (r2 - l2) * 0.02f;
      r_border = (r2 - l2) * 0.02f;
      b_border = (t2 - b2) * 0.015f;
      t_border = (t2 - b2) * 0.01f;
      glow_width_ = r2 - l2 + l_border + r_border;
      glow_height_ = t2 - b2 + b_border + t_border;
      glow_center_x_ = l2 - l_border + 0.5f * glow_width_;
      glow_center_y_ = b2 - b_border + 0.5f * glow_height_;
      glow_dirty_ = false;
    }

    SimpleComponent c(pass);
    c.SetTransparent(true);
    c.SetPremultiplied(true);
    c.SetColor(0.4f * m, 0.5f * m, 0.05f * m, 0.0f);
    c.SetTexture(g_media->GetTexture(SystemTextureID::kScrollWidgetGlow));
    c.PushTransform();
    c.Translate(glow_center_x_, glow_center_y_, 0.9f);
    c.Scale(glow_width_, glow_height_, 0.1f);
    c.DrawModel(g_media->GetModel(SystemModelID::kSoftEdgeOutside));
    c.PopTransform();
    c.Submit();
  }
}

}  // namespace ballistica
