import math
import pyray as rl
from enum import IntEnum
from openpilot.system.ui.lib.application import gui_app, MouseEvent
from openpilot.common.filter_simple import FirstOrderFilter

# Scroll constants for smooth scrolling behavior
MOUSE_WHEEL_SCROLL_SPEED = 50
BOUNCE_RETURN_RATE = 5              # ~0.92 at 60fps
MIN_VELOCITY = 2                    # px/s, changes from auto scroll to steady state
MIN_VELOCITY_FOR_CLICKING = 2 * 60  # px/s, accepts clicks while auto scrolling below this velocity
DRAG_THRESHOLD = 12                 # pixels of movement to consider it a drag, not a click

DEBUG = False


class ScrollState(IntEnum):
  IDLE = 0              # Not dragging, content may be bouncing or scrolling with inertia
  DRAGGING_CONTENT = 1  # User is actively dragging the content


class GuiScrollPanel:
  def __init__(self):
    self._scroll_state: ScrollState = ScrollState.IDLE
    self._last_mouse_y: float = 0.0
    self._start_mouse_y: float = 0.0  # Track the initial mouse position for drag detection
    self._offset_filter_y = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)
    self._velocity_filter_y = FirstOrderFilter(0.0, 0.05, 1 / gui_app.target_fps)
    self._last_drag_time: float = 0.0

  def update(self, bounds: rl.Rectangle, content: rl.Rectangle) -> float:
    for mouse_event in gui_app.mouse_events:
      if mouse_event.slot == 0:
        self._handle_mouse_event(mouse_event, bounds, content)

    self._update_state(bounds, content)

    return float(self._offset_filter_y.x)

  def _update_state(self, bounds: rl.Rectangle, content: rl.Rectangle):
    if DEBUG:
      rl.draw_rectangle_lines(0, 0, abs(int(self._velocity_filter_y.x)), 10, rl.RED)

    # Handle mouse wheel
    self._offset_filter_y.x += rl.get_mouse_wheel_move() * MOUSE_WHEEL_SCROLL_SPEED

    max_scroll_distance = max(0, content.height - bounds.height)
    if self._scroll_state == ScrollState.IDLE:
      above_bounds, below_bounds = self._check_bounds(bounds, content)

      # Decay velocity when idle
      if abs(self._velocity_filter_y.x) > MIN_VELOCITY:
        # Faster decay if bouncing back from out of bounds
        friction = math.exp(-BOUNCE_RETURN_RATE * 1 / gui_app.target_fps)
        self._velocity_filter_y.x *= friction ** 2 if (above_bounds or below_bounds) else friction
      else:
        self._velocity_filter_y.x = 0.0

      if above_bounds or below_bounds:
        if above_bounds:
          self._offset_filter_y.update(0)
        else:
          self._offset_filter_y.update(-max_scroll_distance)

      self._offset_filter_y.x += self._velocity_filter_y.x / gui_app.target_fps

    elif self._scroll_state == ScrollState.DRAGGING_CONTENT:
      # Mouse not moving, decay velocity
      if not len(gui_app.mouse_events):
        self._velocity_filter_y.update(0.0)

    # Settle to exact bounds
    if abs(self._offset_filter_y.x) < 1e-2:
      self._offset_filter_y.x = 0.0
    elif abs(self._offset_filter_y.x + max_scroll_distance) < 1e-2:
      self._offset_filter_y.x = -max_scroll_distance

  def _handle_mouse_event(self, mouse_event: MouseEvent, bounds: rl.Rectangle, content: rl.Rectangle):
    if self._scroll_state == ScrollState.IDLE:
      if rl.check_collision_point_rec(mouse_event.pos, bounds):
        if mouse_event.left_pressed:
          self._start_mouse_y = mouse_event.pos.y
          # Interrupt scrolling with new drag
          # TODO: stop scrolling with any tap, need to fix is_touch_valid
          if abs(self._velocity_filter_y.x) > MIN_VELOCITY_FOR_CLICKING:
            self._scroll_state = ScrollState.DRAGGING_CONTENT
            # Start velocity at initial measurement for more immediate response
            self._velocity_filter_y.initialized = False

        if mouse_event.left_down:
          if abs(mouse_event.pos.y - self._start_mouse_y) > DRAG_THRESHOLD:
            self._scroll_state = ScrollState.DRAGGING_CONTENT
            # Start velocity at initial measurement for more immediate response
            self._velocity_filter_y.initialized = False

    elif self._scroll_state == ScrollState.DRAGGING_CONTENT:
      if mouse_event.left_released:
        self._scroll_state = ScrollState.IDLE
      else:
        delta_y = mouse_event.pos.y - self._last_mouse_y
        above_bounds, below_bounds = self._check_bounds(bounds, content)
        # Rubber banding effect when out of bands
        if above_bounds or below_bounds:
          delta_y /= 3

        self._offset_filter_y.x += delta_y

        # Track velocity for inertia
        dt = mouse_event.t - self._last_drag_time
        if dt > 0:
          drag_velocity = delta_y / dt
          self._velocity_filter_y.update(drag_velocity)

        # TODO: just store last mouse event!
    self._last_drag_time = mouse_event.t
    self._last_mouse_y = mouse_event.pos.y

  def _check_bounds(self, bounds: rl.Rectangle, content: rl.Rectangle) -> tuple[bool, bool]:
    max_scroll_distance = max(0, content.height - bounds.height)
    above_bounds = self._offset_filter_y.x > 0
    below_bounds = self._offset_filter_y.x < -max_scroll_distance
    return above_bounds, below_bounds

  def is_touch_valid(self):
    return self._scroll_state == ScrollState.IDLE and abs(self._velocity_filter_y.x) < MIN_VELOCITY_FOR_CLICKING
