use std::{
    any::Any,
    fmt::Debug,
    ops::Not,
    time::{Duration, Instant},
};

use gpui::{
    Along, Anchor, App, AppContext as _, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask,
    Context, Corners, CursorStyle, DispatchPhase, Div, Edges, Element, ElementId, Entity, EntityId,
    GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
    LayoutId, ListState, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement,
    Pixels, Point, Position, Render, ScrollHandle, ScrollWheelEvent, Size, Stateful,
    StatefulInteractiveElement, Style, Styled, Task, UniformListDecoration,
    UniformListScrollHandle, Window, ease_in_out, prelude::FluentBuilder as _, px, quad, relative,
    size,
};
use gpui_util::ResultExt;
use smallvec::SmallVec;
use theme::ActiveTheme as _;

use std::ops::Range;

use crate::scrollbars::{ScrollbarAutoHide, ScrollbarVisibility, ShowScrollbar};

const SCROLLBAR_HIDE_DELAY_INTERVAL: Duration = Duration::from_secs(1);
const SCROLLBAR_HIDE_DURATION: Duration = Duration::from_millis(400);
const SCROLLBAR_SHOW_DURATION: Duration = Duration::from_millis(50);

pub const EDITOR_SCROLLBAR_WIDTH: Pixels = ScrollbarStyle::Editor.to_pixels();
const SCROLLBAR_PADDING: Pixels = px(4.);
const BORDER_WIDTH: Pixels = px(1.);

pub mod scrollbars {
    use gpui::{App, Global};
    use schemars::JsonSchema;
    use serde::{Deserialize, Serialize};

    /// When to show the scrollbar in the editor.
    ///
    /// Default: auto
    #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
    #[serde(rename_all = "snake_case")]
    pub enum ShowScrollbar {
        /// Show the scrollbar if there's important information or
        /// follow the system's configured behavior.
        #[default]
        Auto,
        /// Match the system's configured behavior.
        System,
        /// Always show the scrollbar.
        Always,
        /// Never show the scrollbar.
        Never,
    }

    pub trait ScrollbarVisibility: 'static {
        fn visibility(&self, cx: &App) -> ShowScrollbar;
    }

    #[derive(Default)]
    pub struct ScrollbarAutoHide(pub bool);

    impl ScrollbarAutoHide {
        pub fn should_hide(&self) -> bool {
            self.0
        }
    }

    impl Global for ScrollbarAutoHide {}
}

fn get_scrollbar_state<T>(
    mut config: Scrollbars<T>,
    caller_location: &'static std::panic::Location,
    window: &mut Window,
    cx: &mut App,
) -> Entity<ScrollbarStateWrapper<T>>
where
    T: ScrollableHandle,
{
    let element_id = config.id.take().unwrap_or_else(|| caller_location.into());
    let track_color = config.track_color;
    let has_border = config.border;

    let state = window.use_keyed_state(element_id, cx, |_, cx| {
        let parent_id = cx.entity_id();
        ScrollbarStateWrapper(cx.new(|cx| ScrollbarState::new_from_config(config, parent_id, cx)))
    });

    state.update(cx, |state, cx| {
        state.0.update(cx, |state, _cx| {
            state.update_colors(track_color, has_border)
        })
    });
    state
}

pub trait WithScrollbar: Sized {
    type Output;

    fn custom_scrollbars<T>(
        self,
        config: Scrollbars<T>,
        window: &mut Window,
        cx: &mut App,
    ) -> Self::Output
    where
        T: ScrollableHandle;

    // TODO: account for these cases properly
    // #[track_caller]
    // fn horizontal_scrollbar(self, window: &mut Window, cx: &mut App) -> Self::Output {
    //     self.custom_scrollbars(
    //         Scrollbars::new(ScrollAxes::Horizontal).ensure_id(core::panic::Location::caller()),
    //         window,
    //         cx,
    //     )
    // }

    // #[track_caller]
    // fn vertical_scrollbar(self, window: &mut Window, cx: &mut App) -> Self::Output {
    //     self.custom_scrollbars(
    //         Scrollbars::new(ScrollAxes::Vertical).ensure_id(core::panic::Location::caller()),
    //         window,
    //         cx,
    //     )
    // }

    #[track_caller]
    fn vertical_scrollbar_for<ScrollHandle: ScrollableHandle + Clone>(
        self,
        scroll_handle: &ScrollHandle,
        window: &mut Window,
        cx: &mut App,
    ) -> Self::Output {
        self.custom_scrollbars(
            Scrollbars::new(ScrollAxes::Vertical)
                .tracked_scroll_handle(scroll_handle)
                .ensure_id(core::panic::Location::caller()),
            window,
            cx,
        )
    }
}

impl WithScrollbar for Stateful<Div> {
    type Output = Self;

    #[track_caller]
    fn custom_scrollbars<T>(
        self,
        config: Scrollbars<T>,
        window: &mut Window,
        cx: &mut App,
    ) -> Self::Output
    where
        T: ScrollableHandle,
    {
        render_scrollbar(
            get_scrollbar_state(config, std::panic::Location::caller(), window, cx),
            self,
            cx,
        )
    }
}

impl WithScrollbar for Div {
    type Output = Stateful<Div>;

    #[track_caller]
    fn custom_scrollbars<T>(
        self,
        config: Scrollbars<T>,
        window: &mut Window,
        cx: &mut App,
    ) -> Self::Output
    where
        T: ScrollableHandle,
    {
        let scrollbar = get_scrollbar_state(config, std::panic::Location::caller(), window, cx);
        // We know this ID stays consistent as long as the element is rendered for
        // consecutive frames, which is sufficient for our use case here
        let scrollbar_entity_id = scrollbar.entity_id();

        render_scrollbar(
            scrollbar,
            self.id(("track-scroll", scrollbar_entity_id)),
            cx,
        )
    }
}

fn render_scrollbar<T>(
    scrollbar: Entity<ScrollbarStateWrapper<T>>,
    div: Stateful<Div>,
    cx: &App,
) -> Stateful<Div>
where
    T: ScrollableHandle,
{
    let state = &scrollbar.read(cx).0;

    div.when_some(state.read(cx).handle_to_track(), |this, handle| {
        this.track_scroll(handle).when_some(
            state.read(cx).visible_axes(),
            |this, axes| match axes {
                ScrollAxes::Horizontal => this.overflow_x_scroll(),
                ScrollAxes::Vertical => this.overflow_y_scroll(),
                ScrollAxes::Both => this.overflow_scroll(),
            },
        )
    })
    .when_some(
        state
            .read(cx)
            .space_to_reserve_for(ScrollbarAxis::Horizontal),
        |this, space| this.pb(space),
    )
    .when_some(
        state.read(cx).space_to_reserve_for(ScrollbarAxis::Vertical),
        |this, space| this.pr(space),
    )
    .child(state.clone())
}

impl<T: ScrollableHandle> UniformListDecoration for ScrollbarStateWrapper<T> {
    fn compute(
        &self,
        _visible_range: Range<usize>,
        _bounds: Bounds<Pixels>,
        scroll_offset: Point<Pixels>,
        _item_height: Pixels,
        _item_count: usize,
        _window: &mut Window,
        _cx: &mut App,
    ) -> gpui::AnyElement {
        ScrollbarElement {
            origin: -scroll_offset,
            state: self.0.clone(),
        }
        .into_any()
    }
}

// impl WithScrollbar for UniformList {
//     type Output = Self;

//     #[track_caller]
//     fn custom_scrollbars<S, T>(
//         self,
//         config: Scrollbars<S, T>,
//         window: &mut Window,
//         cx: &mut App,
//     ) -> Self::Output
//     where
//         S: ScrollbarVisibilitySetting,
//         T: ScrollableHandle,
//     {
//         let scrollbar = get_scrollbar_state(config, std::panic::Location::caller(), window, cx);
//         self.when_some(
//             scrollbar.read_with(cx, |wrapper, cx| {
//                 wrapper
//                     .0
//                     .read(cx)
//                     .handle_to_track::<UniformListScrollHandle>()
//                     .cloned()
//             }),
//             |this, handle| this.track_scroll(handle),
//         )
//         .with_decoration(scrollbar)
//     }
// }

#[derive(Copy, Clone, PartialEq, Eq)]
enum ShowBehavior {
    Always,
    Autohide,
    Never,
}

impl ShowBehavior {
    fn from_setting(setting: ShowScrollbar, cx: &mut App) -> Self {
        match setting {
            ShowScrollbar::Never => Self::Never,
            ShowScrollbar::Auto => Self::Autohide,
            ShowScrollbar::System => {
                if cx.default_global::<ScrollbarAutoHide>().should_hide() {
                    Self::Autohide
                } else {
                    Self::Always
                }
            }
            ShowScrollbar::Always => Self::Always,
        }
    }
}

pub enum ScrollAxes {
    Horizontal,
    Vertical,
    Both,
}

impl ScrollAxes {
    fn apply_to<T>(self, point: Point<T>, value: T) -> Point<T>
    where
        T: Debug + Default + PartialEq + Clone,
    {
        match self {
            Self::Horizontal => point.apply_along(ScrollbarAxis::Horizontal, |_| value),
            Self::Vertical => point.apply_along(ScrollbarAxis::Vertical, |_| value),
            Self::Both => Point::new(value.clone(), value),
        }
    }
}

#[derive(Clone, Debug, Default, PartialEq)]
enum ReservedSpace {
    #[default]
    None,
    Thumb,
    Track,
    StableTrack,
}

impl ReservedSpace {
    fn is_visible(&self) -> bool {
        *self != ReservedSpace::None
    }

    fn needs_scroll_track(&self) -> bool {
        matches!(self, Self::Track | Self::StableTrack)
    }

    fn needs_space_reserved(&self, max_offset: Pixels) -> bool {
        match self {
            Self::StableTrack => true,
            Self::Track => !max_offset.is_zero(),
            _ => false,
        }
    }
}

#[derive(Clone)]
enum Handle<T: ScrollableHandle> {
    Tracked(T),
    Untracked(fn() -> T),
}

#[derive(Clone, Copy, Default, PartialEq)]
pub enum ScrollbarStyle {
    #[default]
    Regular,
    Editor,
}

impl ScrollbarStyle {
    pub const fn to_pixels(&self) -> Pixels {
        match self {
            ScrollbarStyle::Regular => px(6.),
            ScrollbarStyle::Editor => px(15.),
        }
    }
}

#[derive(Clone)]
pub struct Scrollbars<T: ScrollableHandle = ScrollHandle> {
    id: Option<ElementId>,
    get_visibility: fn(&App) -> ShowScrollbar,
    tracked_entity: Option<Option<EntityId>>,
    scrollable_handle: Handle<T>,
    visibility: Point<ReservedSpace>,
    style: Option<ScrollbarStyle>,
    track_color: Option<Hsla>,
    border: bool,
}

impl Scrollbars {
    pub fn new(show_along: ScrollAxes) -> Self {
        Self::new_with_setting(show_along, |_| ShowScrollbar::default())
    }

    pub fn always_visible(show_along: ScrollAxes) -> Self {
        Self::new_with_setting(show_along, |_| ShowScrollbar::Always)
    }

    pub fn for_settings<S: ScrollbarVisibility + Default>() -> Scrollbars {
        Scrollbars::new_with_setting(ScrollAxes::Both, |cx| S::default().visibility(cx))
    }
}

impl Scrollbars {
    fn new_with_setting(show_along: ScrollAxes, get_visibility: fn(&App) -> ShowScrollbar) -> Self {
        Self {
            id: None,
            get_visibility,
            scrollable_handle: Handle::Untracked(ScrollHandle::new),
            tracked_entity: None,
            visibility: show_along.apply_to(Default::default(), ReservedSpace::Thumb),
            style: None,
            track_color: None,
            border: false,
        }
    }
}

impl<ScrollHandle: ScrollableHandle> Scrollbars<ScrollHandle> {
    pub fn id(mut self, id: impl Into<ElementId>) -> Self {
        self.id = Some(id.into());
        self
    }

    fn ensure_id(mut self, id: impl Into<ElementId>) -> Self {
        if self.id.is_none() {
            self.id = Some(id.into());
        }
        self
    }

    /// Notify the current context whenever this scrollbar gets a scroll event
    pub fn notify_content(mut self) -> Self {
        self.tracked_entity = Some(None);
        self
    }

    /// Set a parent model which should be notified whenever this scrollbar gets a scroll event.
    pub fn tracked_entity(mut self, entity_id: EntityId) -> Self {
        self.tracked_entity = Some(Some(entity_id));
        self
    }

    pub fn tracked_scroll_handle<TrackedHandle: ScrollableHandle>(
        self,
        tracked_scroll_handle: &TrackedHandle,
    ) -> Scrollbars<TrackedHandle> {
        let Self {
            id,
            tracked_entity: tracked_entity_id,
            visibility,
            get_visibility,
            track_color,
            border,
            style,
            ..
        } = self;

        Scrollbars {
            scrollable_handle: Handle::Tracked(tracked_scroll_handle.clone()),
            id,
            tracked_entity: tracked_entity_id,
            visibility,
            track_color,
            border,
            get_visibility,
            style,
        }
    }

    pub fn show_along(mut self, along: ScrollAxes) -> Self {
        self.visibility = along.apply_to(self.visibility, ReservedSpace::Thumb);
        self
    }

    pub fn style(mut self, style: ScrollbarStyle) -> Self {
        self.style = Some(style);
        self
    }

    pub fn with_track_along(mut self, along: ScrollAxes, background_color: Hsla) -> Self {
        self.visibility = along.apply_to(self.visibility, ReservedSpace::Track);
        self.track_color = Some(background_color);
        self
    }

    pub fn with_stable_track_along(mut self, along: ScrollAxes, background_color: Hsla) -> Self {
        self.visibility = along.apply_to(self.visibility, ReservedSpace::StableTrack);
        self.track_color = Some(background_color);
        self.border = true;
        self
    }
}

#[derive(PartialEq, Clone, Debug)]
enum VisibilityState {
    Visible,
    Animating { showing: bool, delta: f32 },
    ThumbHidden,
    Hidden,
    Disabled,
}

enum AnimationState {
    InProgress {
        current_delta: f32,
        animation_duration: Duration,
        showing: bool,
    },
    Stale,
}

const DELTA_MAX: f32 = 1.0;

impl VisibilityState {
    fn from_behavior(behavior: ShowBehavior) -> Self {
        match behavior {
            ShowBehavior::Always => Self::Visible,
            ShowBehavior::Never => Self::Disabled,
            ShowBehavior::Autohide => Self::for_show(),
        }
    }

    fn for_show() -> Self {
        Self::Animating {
            showing: true,
            delta: Default::default(),
        }
    }

    fn for_autohide() -> Self {
        Self::Animating {
            showing: Default::default(),
            delta: Default::default(),
        }
    }

    fn is_visible(&self) -> bool {
        matches!(
            self,
            Self::Visible | Self::Animating { .. } | Self::ThumbHidden
        )
    }

    #[inline]
    fn is_disabled(&self) -> bool {
        *self == VisibilityState::Disabled
    }

    fn animation_state(&self) -> Option<AnimationState> {
        match self {
            Self::ThumbHidden => Some(AnimationState::Stale),
            Self::Animating { showing, delta } => Some(AnimationState::InProgress {
                current_delta: *delta,
                animation_duration: if *showing {
                    SCROLLBAR_SHOW_DURATION
                } else {
                    SCROLLBAR_HIDE_DURATION
                },
                showing: *showing,
            }),
            _ => None,
        }
    }

    fn set_delta(&mut self, new_delta: f32, keep_track_visible: bool) {
        match self {
            Self::Animating { showing, delta } if new_delta >= DELTA_MAX => {
                if *showing {
                    *self = Self::Visible;
                } else if keep_track_visible {
                    *self = Self::ThumbHidden;
                } else {
                    *self = Self::Hidden;
                }
            }
            Self::Animating { delta, .. } => *delta = new_delta,
            _ => {}
        }
    }

    fn toggle_visible(&self, show_behavior: ShowBehavior) -> Self {
        match self {
            Self::Hidden | Self::ThumbHidden => {
                if show_behavior == ShowBehavior::Autohide {
                    Self::for_show()
                } else {
                    Self::Visible
                }
            }
            Self::Animating {
                showing: false,
                delta: progress,
            } => Self::Animating {
                showing: true,
                delta: DELTA_MAX - progress,
            },
            _ => self.clone(),
        }
    }
}

enum ParentHoverEvent {
    Within,
    Entered,
    Exited,
    Outside,
}

#[derive(Clone)]
struct TrackColors {
    background: Hsla,
    has_border: bool,
}

pub fn on_new_scrollbars<T: gpui::Global>(cx: &mut App) {
    cx.observe_new::<ScrollbarState>(|_, window, cx| {
        if let Some(window) = window {
            cx.observe_global_in::<T>(window, ScrollbarState::settings_changed)
                .detach();
        }
    })
    .detach();
}

/// This is used to ensure notifies within the state do not notify the parent
/// unintentionally.
struct ScrollbarStateWrapper<T: ScrollableHandle>(Entity<ScrollbarState<T>>);

/// A scrollbar state that should be persisted across frames.
struct ScrollbarState<T: ScrollableHandle = ScrollHandle> {
    thumb_state: ThumbState,
    notify_id: Option<EntityId>,
    manually_added: bool,
    scroll_handle: T,
    show_behavior: ShowBehavior,
    get_visibility: fn(&App) -> ShowScrollbar,
    visibility: Point<ReservedSpace>,
    track_color: Option<TrackColors>,
    show_state: VisibilityState,
    style: ScrollbarStyle,
    mouse_in_parent: bool,
    last_prepaint_state: Option<ScrollbarPrepaintState>,
    _auto_hide_task: Option<Task<()>>,
}

impl<T: ScrollableHandle> ScrollbarState<T> {
    fn new_from_config(config: Scrollbars<T>, parent_id: EntityId, cx: &mut Context<Self>) -> Self {
        let (manually_added, scroll_handle) = match config.scrollable_handle {
            Handle::Tracked(handle) => (true, handle),
            Handle::Untracked(func) => (false, func()),
        };

        let show_behavior = ShowBehavior::from_setting((config.get_visibility)(cx), cx);
        ScrollbarState {
            thumb_state: Default::default(),
            notify_id: config.tracked_entity.map(|id| id.unwrap_or(parent_id)),
            manually_added,
            scroll_handle,
            visibility: config.visibility,
            track_color: config.track_color.map(|color| TrackColors {
                background: color,
                has_border: config.border,
            }),
            show_behavior,
            get_visibility: config.get_visibility,
            style: config.style.unwrap_or_default(),
            show_state: VisibilityState::from_behavior(show_behavior),
            mouse_in_parent: true,
            last_prepaint_state: None,
            _auto_hide_task: None,
        }
    }

    fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        self.set_show_behavior(
            ShowBehavior::from_setting((self.get_visibility)(cx), cx),
            window,
            cx,
        );
    }

    /// Schedules a scrollbar auto hide if no auto hide is currently in progress yet.
    fn schedule_auto_hide(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        if self._auto_hide_task.is_none() {
            self._auto_hide_task = (self.visible() && self.show_behavior == ShowBehavior::Autohide)
                .then(|| {
                    cx.spawn_in(window, async move |scrollbar_state, cx| {
                        cx.background_executor()
                            .timer(SCROLLBAR_HIDE_DELAY_INTERVAL)
                            .await;
                        scrollbar_state
                            .update(cx, |state, cx| {
                                if state.thumb_state == ThumbState::Inactive {
                                    state.set_visibility(VisibilityState::for_autohide(), cx);
                                }
                                state._auto_hide_task.take();
                            })
                            .log_err();
                    })
                });
        }
    }

    fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        let visibility = self.show_state.toggle_visible(self.show_behavior);
        self.set_visibility(visibility, cx);
        self._auto_hide_task.take();
        self.schedule_auto_hide(window, cx);
    }

    fn set_show_behavior(
        &mut self,
        behavior: ShowBehavior,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        if self.show_behavior != behavior {
            self.show_behavior = behavior;
            self.set_visibility(VisibilityState::from_behavior(behavior), cx);
            self.schedule_auto_hide(window, cx);
            cx.notify();
        }
    }

    fn set_visibility(&mut self, visibility: VisibilityState, cx: &mut Context<Self>) {
        if self.show_state != visibility {
            self.show_state = visibility;
            cx.notify();
        }
    }

    #[inline]
    fn visible_axes(&self) -> Option<ScrollAxes> {
        match (&self.visibility.x, &self.visibility.y) {
            (ReservedSpace::None, ReservedSpace::None) => None,
            (ReservedSpace::None, _) => Some(ScrollAxes::Vertical),
            (_, ReservedSpace::None) => Some(ScrollAxes::Horizontal),
            _ => Some(ScrollAxes::Both),
        }
    }

    fn space_to_reserve_for(&self, axis: ScrollbarAxis) -> Option<Pixels> {
        (self.show_state.is_disabled().not()
            && self
                .visibility
                .along(axis)
                .needs_space_reserved(self.scroll_handle().max_offset().along(axis)))
        .then(|| self.space_to_reserve())
    }

    fn space_to_reserve(&self) -> Pixels {
        self.style.to_pixels() + 2 * SCROLLBAR_PADDING
    }

    fn handle_to_track<Handle: ScrollableHandle>(&self) -> Option<&Handle> {
        (!self.manually_added)
            .then(|| (self.scroll_handle() as &dyn Any).downcast_ref::<Handle>())
            .flatten()
    }

    fn scroll_handle(&self) -> &T {
        &self.scroll_handle
    }

    fn set_offset(&mut self, offset: Point<Pixels>, cx: &mut Context<Self>) {
        self.scroll_handle.set_offset(offset);
        self.notify_parent(cx);
        cx.notify();
    }

    fn is_dragging(&self) -> bool {
        self.thumb_state.is_dragging()
    }

    fn set_dragging(
        &mut self,
        axis: ScrollbarAxis,
        drag_offset: Pixels,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.set_thumb_state(ThumbState::Dragging(axis, drag_offset), window, cx);
        self.scroll_handle().drag_started();
    }

    fn update_hovered_thumb(
        &mut self,
        position: &Point<Pixels>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        self.set_thumb_state(
            if let Some(&ScrollbarLayout { axis, .. }) =
                self.last_prepaint_state.as_ref().and_then(|state| {
                    state
                        .thumb_for_position(position)
                        .filter(|thumb| thumb.cursor_hitbox.is_hovered(window))
                })
            {
                ThumbState::Hover(axis)
            } else {
                ThumbState::Inactive
            },
            window,
            cx,
        );
    }

    fn set_thumb_state(&mut self, state: ThumbState, window: &mut Window, cx: &mut Context<Self>) {
        if self.thumb_state != state {
            if state == ThumbState::Inactive {
                self.schedule_auto_hide(window, cx);
            } else {
                self.set_visibility(self.show_state.toggle_visible(self.show_behavior), cx);
                self._auto_hide_task.take();
            }
            self.thumb_state = state;
            cx.notify();
        }
    }

    fn update_parent_hovered(&mut self, window: &Window) -> ParentHoverEvent {
        let last_parent_hovered = self.mouse_in_parent;
        self.mouse_in_parent = self.parent_hovered(window);
        let state_changed = self.mouse_in_parent != last_parent_hovered;
        match (self.mouse_in_parent, state_changed) {
            (true, true) => ParentHoverEvent::Entered,
            (true, false) => ParentHoverEvent::Within,
            (false, true) => ParentHoverEvent::Exited,
            (false, false) => ParentHoverEvent::Outside,
        }
    }

    fn update_colors(&mut self, track_color: Option<Hsla>, has_border: bool) {
        self.track_color = track_color.map(|color| TrackColors {
            background: color,
            has_border,
        });
    }

    fn parent_hovered(&self, window: &Window) -> bool {
        self.last_prepaint_state
            .as_ref()
            .is_some_and(|state| state.parent_bounds_hitbox.is_hovered(window))
    }

    fn hit_for_position(&self, position: &Point<Pixels>) -> Option<&ScrollbarLayout> {
        self.last_prepaint_state
            .as_ref()
            .and_then(|state| state.hit_for_position(position))
    }

    fn thumb_for_axis(&self, axis: ScrollbarAxis) -> Option<&ScrollbarLayout> {
        self.last_prepaint_state
            .as_ref()
            .and_then(|state| state.thumbs.iter().find(|thumb| thumb.axis == axis))
    }

    fn thumb_ranges(
        &self,
    ) -> impl Iterator<Item = (ScrollbarAxis, Range<f32>, ReservedSpace)> + '_ {
        const MINIMUM_THUMB_SIZE: Pixels = px(25.);
        let max_offset = self.scroll_handle().max_offset();
        let viewport_size = self.scroll_handle().viewport().size;
        let current_offset = self.scroll_handle().offset();

        [ScrollbarAxis::Horizontal, ScrollbarAxis::Vertical]
            .into_iter()
            .filter(|&axis| self.visibility.along(axis).is_visible())
            .flat_map(move |axis| {
                let max_offset = max_offset.along(axis);
                let viewport_size = viewport_size.along(axis);
                if max_offset.is_zero() || viewport_size.is_zero() {
                    return None;
                }
                let content_size = viewport_size + max_offset;
                let visible_percentage = viewport_size / content_size;
                let thumb_size = MINIMUM_THUMB_SIZE.max(viewport_size * visible_percentage);
                if thumb_size > viewport_size {
                    return None;
                }
                let current_offset = current_offset
                    .along(axis)
                    .clamp(-max_offset, Pixels::ZERO)
                    .abs();
                let start_offset = (current_offset / max_offset) * (viewport_size - thumb_size);
                let thumb_percentage_start = start_offset / viewport_size;
                let thumb_percentage_end = (start_offset + thumb_size) / viewport_size;
                Some((
                    axis,
                    thumb_percentage_start..thumb_percentage_end,
                    self.visibility.along(axis),
                ))
            })
    }

    fn visible(&self) -> bool {
        self.show_state.is_visible()
    }

    #[inline]
    fn disabled(&self) -> bool {
        self.show_state.is_disabled()
    }

    fn notify_parent(&self, cx: &mut App) {
        if let Some(entity_id) = self.notify_id {
            cx.notify(entity_id);
        }
    }
}

impl<T: ScrollableHandle> Render for ScrollbarState<T> {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        ScrollbarElement {
            state: cx.entity(),
            origin: Default::default(),
        }
    }
}

struct ScrollbarElement<T: ScrollableHandle> {
    origin: Point<Pixels>,
    state: Entity<ScrollbarState<T>>,
}

#[derive(Default, Debug, PartialEq, Eq)]
enum ThumbState {
    #[default]
    Inactive,
    Hover(ScrollbarAxis),
    Dragging(ScrollbarAxis, Pixels),
}

impl ThumbState {
    fn is_dragging(&self) -> bool {
        matches!(*self, ThumbState::Dragging(..))
    }
}

impl ScrollableHandle for UniformListScrollHandle {
    fn max_offset(&self) -> Point<Pixels> {
        self.0.borrow().base_handle.max_offset()
    }

    fn set_offset(&self, point: Point<Pixels>) {
        self.0.borrow().base_handle.set_offset(point);
    }

    fn offset(&self) -> Point<Pixels> {
        self.0.borrow().base_handle.offset()
    }

    fn viewport(&self) -> Bounds<Pixels> {
        self.0.borrow().base_handle.bounds()
    }
}

impl ScrollableHandle for ListState {
    fn max_offset(&self) -> Point<Pixels> {
        self.max_offset_for_scrollbar()
    }

    fn set_offset(&self, point: Point<Pixels>) {
        self.set_offset_from_scrollbar(point);
    }

    fn offset(&self) -> Point<Pixels> {
        self.scroll_px_offset_for_scrollbar()
    }

    fn drag_started(&self) {
        self.scrollbar_drag_started();
    }

    fn drag_ended(&self) {
        self.scrollbar_drag_ended();
    }

    fn viewport(&self) -> Bounds<Pixels> {
        self.viewport_bounds()
    }
}

impl ScrollableHandle for ScrollHandle {
    fn max_offset(&self) -> Point<Pixels> {
        self.max_offset()
    }

    fn set_offset(&self, point: Point<Pixels>) {
        self.set_offset(point);
    }

    fn offset(&self) -> Point<Pixels> {
        self.offset()
    }

    fn viewport(&self) -> Bounds<Pixels> {
        self.bounds()
    }
}

pub trait ScrollableHandle: 'static + Any + Sized + Clone {
    fn max_offset(&self) -> Point<Pixels>;
    fn set_offset(&self, point: Point<Pixels>);
    fn offset(&self) -> Point<Pixels>;
    fn viewport(&self) -> Bounds<Pixels>;
    fn drag_started(&self) {}
    fn drag_ended(&self) {}

    fn scrollable_along(&self, axis: ScrollbarAxis) -> bool {
        self.max_offset().along(axis) > Pixels::ZERO
    }
    fn content_size(&self) -> Size<Pixels> {
        self.viewport().size + self.max_offset().into()
    }
}

enum ScrollbarMouseEvent {
    TrackClick,
    ThumbDrag(Pixels),
}

struct ScrollbarLayout {
    thumb_bounds: Bounds<Pixels>,
    track_bounds: Bounds<Pixels>,
    cursor_hitbox: Hitbox,
    reserved_space: ReservedSpace,
    track_config: Option<(Bounds<Pixels>, TrackColors)>,
    axis: ScrollbarAxis,
}

impl ScrollbarLayout {
    fn compute_click_offset(
        &self,
        event_position: Point<Pixels>,
        max_offset: Point<Pixels>,
        event_type: ScrollbarMouseEvent,
    ) -> Pixels {
        let Self {
            track_bounds,
            thumb_bounds,
            axis,
            ..
        } = self;
        let axis = *axis;

        let viewport_size = track_bounds.size.along(axis);
        let thumb_size = thumb_bounds.size.along(axis);
        let thumb_offset = match event_type {
            ScrollbarMouseEvent::TrackClick => thumb_size / 2.,
            ScrollbarMouseEvent::ThumbDrag(thumb_offset) => thumb_offset,
        };

        let thumb_start =
            (event_position.along(axis) - track_bounds.origin.along(axis) - thumb_offset)
                .clamp(px(0.), viewport_size - thumb_size);

        let max_offset = max_offset.along(axis);
        let percentage = if viewport_size > thumb_size {
            thumb_start / (viewport_size - thumb_size)
        } else {
            0.
        };

        -max_offset * percentage
    }
}

impl PartialEq for ScrollbarLayout {
    fn eq(&self, other: &Self) -> bool {
        if self.axis != other.axis {
            return false;
        }

        let axis = self.axis;
        let thumb_offset =
            self.thumb_bounds.origin.along(axis) - self.track_bounds.origin.along(axis);
        let other_thumb_offset =
            other.thumb_bounds.origin.along(axis) - other.track_bounds.origin.along(axis);

        thumb_offset == other_thumb_offset
            && self.thumb_bounds.size.along(axis) == other.thumb_bounds.size.along(axis)
    }
}

pub struct ScrollbarPrepaintState {
    parent_bounds_hitbox: Hitbox,
    thumbs: SmallVec<[ScrollbarLayout; 2]>,
}

impl ScrollbarPrepaintState {
    fn thumb_for_position(&self, position: &Point<Pixels>) -> Option<&ScrollbarLayout> {
        self.thumbs
            .iter()
            .find(|info| info.thumb_bounds.contains(position))
    }

    fn hit_for_position(&self, position: &Point<Pixels>) -> Option<&ScrollbarLayout> {
        self.thumbs.iter().find(|info| {
            if info.reserved_space.needs_scroll_track() {
                info.track_bounds.contains(position)
            } else {
                info.thumb_bounds.contains(position)
            }
        })
    }
}

impl PartialEq for ScrollbarPrepaintState {
    fn eq(&self, other: &Self) -> bool {
        self.thumbs == other.thumbs
    }
}

impl<T: ScrollableHandle> Element for ScrollbarElement<T> {
    type RequestLayoutState = ();
    type PrepaintState = Option<(ScrollbarPrepaintState, Option<f32>)>;

    fn id(&self) -> Option<ElementId> {
        Some(("scrollbar_animation", self.state.entity_id()).into())
    }

    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
        None
    }

    fn request_layout(
        &mut self,
        _id: Option<&GlobalElementId>,
        _inspector_id: Option<&gpui::InspectorElementId>,
        window: &mut Window,
        cx: &mut App,
    ) -> (LayoutId, Self::RequestLayoutState) {
        let scrollbar_style = Style {
            position: Position::Absolute,
            inset: Edges::default(),
            size: size(relative(1.), relative(1.)).map(Into::into),
            ..Default::default()
        };

        (window.request_layout(scrollbar_style, None, cx), ())
    }

    fn prepaint(
        &mut self,
        id: Option<&GlobalElementId>,
        _inspector_id: Option<&gpui::InspectorElementId>,
        bounds: Bounds<Pixels>,
        _request_layout: &mut Self::RequestLayoutState,
        window: &mut Window,
        cx: &mut App,
    ) -> Self::PrepaintState {
        let prepaint_state =
            self.state
                .read(cx)
                .disabled()
                .not()
                .then(|| ScrollbarPrepaintState {
                    thumbs: {
                        let state = self.state.read(cx);
                        let thumb_ranges = state.thumb_ranges().collect::<SmallVec<[_; 2]>>();
                        let width = state.style.to_pixels();
                        let track_color = state.track_color.as_ref();

                        let additional_padding = if thumb_ranges.len() == 2 {
                            width
                        } else {
                            Pixels::ZERO
                        };

                        thumb_ranges
                            .into_iter()
                            .map(|(axis, thumb_range, reserved_space)| {
                                let track_anchor = match axis {
                                    ScrollbarAxis::Horizontal => Anchor::BottomLeft,
                                    ScrollbarAxis::Vertical => Anchor::TopRight,
                                };

                                let scroll_track_bounds = Bounds::from_anchor_and_size(
                                    track_anchor,
                                    self.origin + bounds.corner(track_anchor),
                                    bounds.size.apply_along(axis.invert(), |_| {
                                        width
                                            + match state.style {
                                                ScrollbarStyle::Regular => 2 * SCROLLBAR_PADDING,
                                                ScrollbarStyle::Editor => Pixels::ZERO,
                                            }
                                    }),
                                );

                                let has_border =
                                    track_color.is_some_and(|track_colors| track_colors.has_border);

                                // Rounded style needs a bit of padding, whereas for editor scrollbars,
                                // we want the full length of the track
                                let thumb_container_bounds = match state.style {
                                    ScrollbarStyle::Regular => {
                                        scroll_track_bounds.dilate(-SCROLLBAR_PADDING)
                                    }
                                    ScrollbarStyle::Editor if has_border => scroll_track_bounds
                                        .extend(match axis {
                                            ScrollbarAxis::Horizontal => Edges {
                                                top: -BORDER_WIDTH,
                                                ..Default::default()
                                            },

                                            ScrollbarAxis::Vertical => Edges {
                                                left: -BORDER_WIDTH,
                                                ..Default::default()
                                            },
                                        }),
                                    ScrollbarStyle::Editor => scroll_track_bounds,
                                };

                                let available_space =
                                    thumb_container_bounds.size.along(axis) - additional_padding;

                                let thumb_offset = thumb_range.start * available_space;
                                let thumb_end = thumb_range.end * available_space;
                                let thumb_bounds = Bounds::new(
                                    thumb_container_bounds
                                        .origin
                                        .apply_along(axis, |origin| origin + thumb_offset),
                                    thumb_container_bounds
                                        .size
                                        .apply_along(axis, |_| thumb_end - thumb_offset),
                                );

                                let needs_scroll_track = reserved_space.needs_scroll_track();

                                ScrollbarLayout {
                                    thumb_bounds,
                                    track_bounds: thumb_container_bounds,
                                    axis,
                                    cursor_hitbox: window.insert_hitbox(
                                        if needs_scroll_track {
                                            if has_border && state.style == ScrollbarStyle::Editor {
                                                scroll_track_bounds
                                            } else {
                                                thumb_container_bounds
                                            }
                                        } else {
                                            thumb_bounds
                                        },
                                        HitboxBehavior::BlockMouseExceptScroll,
                                    ),
                                    track_config: track_color
                                        .filter(|_| needs_scroll_track)
                                        .map(|color| (scroll_track_bounds, color.clone())),
                                    reserved_space,
                                }
                            })
                            .collect()
                    },
                    parent_bounds_hitbox: window.insert_hitbox(bounds, HitboxBehavior::Normal),
                });
        if prepaint_state
            .as_ref()
            .is_some_and(|state| Some(state) != self.state.read(cx).last_prepaint_state.as_ref())
        {
            self.state
                .update(cx, |state, cx| state.show_scrollbars(window, cx));
        }

        prepaint_state.map(|state| {
            let autohide_delta = self
                .state
                .read(cx)
                .show_state
                .animation_state()
                .map(|state| match state {
                    AnimationState::InProgress {
                        current_delta,
                        animation_duration: delta_duration,
                        showing: should_invert,
                    } => window.with_element_state(id.unwrap(), |state, window| {
                        let state = state.unwrap_or_else(|| Instant::now());
                        let current = Instant::now();

                        let new_delta = DELTA_MAX.min(
                            current_delta + (current - state).div_duration_f32(delta_duration),
                        );
                        self.state.update(cx, |state, _| {
                            let has_border = state
                                .track_color
                                .as_ref()
                                .is_some_and(|track_colors| track_colors.has_border);
                            state.show_state.set_delta(new_delta, has_border)
                        });

                        window.request_animation_frame();
                        let delta = if should_invert {
                            DELTA_MAX - current_delta
                        } else {
                            current_delta
                        };
                        (ease_in_out(delta), current)
                    }),
                    AnimationState::Stale => 1.0,
                });

            (state, autohide_delta)
        })
    }

    fn paint(
        &mut self,
        _id: Option<&GlobalElementId>,
        _inspector_id: Option<&gpui::InspectorElementId>,
        Bounds { origin, size }: Bounds<Pixels>,
        _request_layout: &mut Self::RequestLayoutState,
        prepaint_state: &mut Self::PrepaintState,
        window: &mut Window,
        cx: &mut App,
    ) {
        let Some((prepaint_state, autohide_fade)) = prepaint_state.take() else {
            return;
        };

        let bounds = Bounds::new(self.origin + origin, size);
        window.with_content_mask(Some(ContentMask { bounds }), |window| {
            let colors = cx.theme().colors();

            let capture_phase;

            if self.state.read(cx).visible() {
                let state = self.state.read(cx);
                let thumb_state = &state.thumb_state;
                let style = state.style;

                if thumb_state.is_dragging() {
                    capture_phase = DispatchPhase::Capture;
                } else {
                    capture_phase = DispatchPhase::Bubble;
                }

                for ScrollbarLayout {
                    thumb_bounds,
                    cursor_hitbox,
                    axis,
                    reserved_space,
                    track_config,
                    ..
                } in &prepaint_state.thumbs
                {
                    const MAXIMUM_OPACITY: f32 = 0.7;
                    let (thumb_base_color, hovered) = match thumb_state {
                        ThumbState::Dragging(dragged_axis, _) if dragged_axis == axis => {
                            (colors.scrollbar_thumb_active_background, false)
                        }
                        ThumbState::Hover(hovered_axis) if hovered_axis == axis => {
                            (colors.scrollbar_thumb_hover_background, true)
                        }
                        _ => (colors.scrollbar_thumb_background, false),
                    };

                    let blend_color = track_config
                        .as_ref()
                        .map(|(_, colors)| colors.background)
                        .unwrap_or(colors.surface_background);

                    let blending_color = if hovered || reserved_space.needs_scroll_track() {
                        blend_color
                    } else {
                        blend_color.min(blend_color.alpha(MAXIMUM_OPACITY))
                    };

                    let mut thumb_color = blending_color.blend(thumb_base_color);

                    if !hovered && let Some(fade) = autohide_fade {
                        thumb_color.fade_out(fade);
                    }

                    if let Some((track_bounds, colors)) = track_config {
                        let has_border = colors.has_border;

                        let mut track_color = colors.background;
                        if let Some(fade) = autohide_fade
                            && !has_border
                        {
                            track_color.fade_out(fade);
                        }

                        let border_edges = has_border
                            .then(|| match axis {
                                ScrollbarAxis::Horizontal => Edges {
                                    top: BORDER_WIDTH,
                                    ..Default::default()
                                },
                                ScrollbarAxis::Vertical => Edges {
                                    left: BORDER_WIDTH,
                                    ..Default::default()
                                },
                            })
                            .unwrap_or_default();

                        let border_color = if has_border {
                            cx.theme().colors().border_variant.opacity(0.6)
                        } else {
                            Hsla::transparent_black()
                        };

                        window.paint_quad(quad(
                            *track_bounds,
                            Corners::default(),
                            track_color,
                            border_edges,
                            border_color,
                            BorderStyle::Solid,
                        ));
                    }

                    window.paint_quad(quad(
                        *thumb_bounds,
                        match style {
                            ScrollbarStyle::Regular => Corners::all(Pixels::MAX)
                                .clamp_radii_for_quad_size(thumb_bounds.size),
                            ScrollbarStyle::Editor => Corners::default(),
                        },
                        thumb_color,
                        Edges::default(),
                        Hsla::transparent_black(),
                        BorderStyle::default(),
                    ));

                    if thumb_state.is_dragging() {
                        window.set_window_cursor_style(CursorStyle::Arrow);
                    } else {
                        window.set_cursor_style(CursorStyle::Arrow, cursor_hitbox);
                    }
                }
            } else {
                capture_phase = DispatchPhase::Bubble;
            }

            self.state.update(cx, |state, _| {
                state.last_prepaint_state = Some(prepaint_state)
            });

            window.on_mouse_event({
                let state = self.state.clone();

                move |event: &MouseDownEvent, phase, window, cx| {
                    state.update(cx, |state, cx| {
                        let Some(scrollbar_layout) = (phase == capture_phase
                            && event.button == MouseButton::Left)
                            .then(|| state.hit_for_position(&event.position))
                            .flatten()
                        else {
                            return;
                        };

                        let ScrollbarLayout {
                            thumb_bounds, axis, ..
                        } = scrollbar_layout;

                        if thumb_bounds.contains(&event.position) {
                            let offset =
                                event.position.along(*axis) - thumb_bounds.origin.along(*axis);
                            state.set_dragging(*axis, offset, window, cx);
                        } else {
                            let scroll_handle = state.scroll_handle();
                            let click_offset = scrollbar_layout.compute_click_offset(
                                event.position,
                                scroll_handle.max_offset(),
                                ScrollbarMouseEvent::TrackClick,
                            );
                            state.set_offset(
                                scroll_handle.offset().apply_along(*axis, |_| click_offset),
                                cx,
                            );
                        };

                        cx.stop_propagation();
                    });
                }
            });

            window.on_mouse_event({
                let state = self.state.clone();

                move |event: &ScrollWheelEvent, phase, window, cx| {
                    state.update(cx, |state, cx| {
                        if phase.capture() && state.parent_hovered(window) {
                            state.update_hovered_thumb(&event.position, window, cx)
                        }
                    });
                }
            });

            window.on_mouse_event({
                let state = self.state.clone();

                move |event: &MouseMoveEvent, phase, window, cx| {
                    if phase != capture_phase {
                        return;
                    }

                    match state.read(cx).thumb_state {
                        ThumbState::Dragging(axis, drag_state) if event.dragging() => {
                            if let Some(scrollbar_layout) = state.read(cx).thumb_for_axis(axis) {
                                let scroll_handle = state.read(cx).scroll_handle();
                                let drag_offset = scrollbar_layout.compute_click_offset(
                                    event.position,
                                    scroll_handle.max_offset(),
                                    ScrollbarMouseEvent::ThumbDrag(drag_state),
                                );
                                let new_offset =
                                    scroll_handle.offset().apply_along(axis, |_| drag_offset);

                                state.update(cx, |state, cx| state.set_offset(new_offset, cx));
                                cx.stop_propagation();
                            }
                        }
                        _ => state.update(cx, |state, cx| {
                            match state.update_parent_hovered(window) {
                                hover @ ParentHoverEvent::Entered
                                | hover @ ParentHoverEvent::Within
                                    if event.pressed_button.is_none() =>
                                {
                                    if matches!(hover, ParentHoverEvent::Entered) {
                                        state.show_scrollbars(window, cx);
                                    }
                                    state.update_hovered_thumb(&event.position, window, cx);
                                    if state.thumb_state != ThumbState::Inactive {
                                        cx.stop_propagation();
                                    }
                                }
                                ParentHoverEvent::Exited => {
                                    state.set_thumb_state(ThumbState::Inactive, window, cx);
                                }
                                _ => {}
                            }
                        }),
                    }
                }
            });

            window.on_mouse_event({
                let state = self.state.clone();
                move |event: &MouseUpEvent, phase, window, cx| {
                    if phase != capture_phase {
                        return;
                    }

                    state.update(cx, |state, cx| {
                        if state.is_dragging() {
                            state.scroll_handle().drag_ended();
                        }

                        if !state.parent_hovered(window) {
                            state.schedule_auto_hide(window, cx);
                            return;
                        }

                        state.update_hovered_thumb(&event.position, window, cx);
                    });
                }
            });
        })
    }
}

impl<T: ScrollableHandle> IntoElement for ScrollbarElement<T> {
    type Element = Self;

    fn into_element(self) -> Self::Element {
        self
    }
}
