use editor::{
    Anchor, Editor, HighlightKey, MultiBufferSnapshot, SelectionEffects, ToPoint,
    scroll::Autoscroll,
};
use gpui::{
    Action, App, AppContext as _, Context, Div, Entity, EntityId, EventEmitter, FocusHandle,
    Focusable, HighlightStyle, Hsla, InteractiveElement, IntoElement, MouseButton, MouseDownEvent,
    MouseMoveEvent, ParentElement, Render, ScrollStrategy, SharedString, Styled, Task,
    UniformListScrollHandle, WeakEntity, Window, actions, div, rems, uniform_list,
};
use language::{BufferId, Point, ToOffset};
use menu::{SelectNext, SelectPrevious};
use std::{mem, ops::Range, sync::Arc, time::Duration};
use theme::ActiveTheme;
use theme::SyntaxTheme;
use ui::{
    ButtonCommon, ButtonLike, ButtonStyle, Color, ContextMenu, FluentBuilder as _, IconButton,
    IconName, IconPosition, IconSize, Label, LabelCommon, LabelSize, PopoverMenu,
    PopoverMenuHandle, StyledExt, Toggleable, Tooltip, WithScrollbar, h_flex, v_flex,
};
use workspace::{
    Event as WorkspaceEvent, SplitDirection, ToolbarItemEvent, ToolbarItemLocation,
    ToolbarItemView, Workspace,
    item::{Item, ItemHandle},
};

actions!(
    dev,
    [
        /// Opens the highlights tree view for the current file.
        OpenHighlightsTreeView,
    ]
);

actions!(
    highlights_tree_view,
    [
        /// Toggles showing text highlights.
        ToggleTextHighlights,
        /// Toggles showing semantic token highlights.
        ToggleSemanticTokens,
        /// Toggles showing syntax token highlights.
        ToggleSyntaxTokens,
    ]
);

pub fn init(cx: &mut App) {
    cx.observe_new(move |workspace: &mut Workspace, _, _| {
        workspace.register_action(move |workspace, _: &OpenHighlightsTreeView, window, cx| {
            let active_item = workspace.active_item(cx);
            let workspace_handle = workspace.weak_handle();
            let highlights_tree_view =
                cx.new(|cx| HighlightsTreeView::new(workspace_handle, active_item, window, cx));
            workspace.split_item(
                SplitDirection::Right,
                Box::new(highlights_tree_view),
                window,
                cx,
            )
        });
    })
    .detach();
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum HighlightCategory {
    Text(HighlightKey),
    SyntaxToken {
        capture_name: SharedString,
        theme_key: Option<SharedString>,
    },
    SemanticToken {
        token_type: Option<SharedString>,
        token_modifiers: Option<SharedString>,
        theme_key: Option<SharedString>,
    },
}

impl HighlightCategory {
    fn label(&self) -> SharedString {
        match self {
            HighlightCategory::Text(key) => format!("text: {key:?}").into(),
            HighlightCategory::SyntaxToken {
                capture_name,
                theme_key: Some(theme_key),
            } => format!("syntax: {capture_name} \u{2192} {theme_key}").into(),
            HighlightCategory::SyntaxToken {
                capture_name,
                theme_key: None,
            } => format!("syntax: {capture_name}").into(),
            HighlightCategory::SemanticToken {
                token_type,
                token_modifiers,
                theme_key,
            } => {
                let label = match (token_type, token_modifiers) {
                    (Some(token_type), Some(modifiers)) => {
                        format!("semantic token: {token_type} [{modifiers}]")
                    }
                    (Some(token_type), None) => format!("semantic token: {token_type}"),
                    (None, Some(modifiers)) => format!("semantic token [{modifiers}]"),
                    (None, None) => "semantic token".to_string(),
                };

                if let Some(theme_key) = theme_key {
                    format!("{label} \u{2192} {theme_key}").into()
                } else {
                    label.into()
                }
            }
        }
    }
}

#[derive(Debug, Clone)]
struct HighlightEntry {
    range: Range<Anchor>,
    buffer_id: BufferId,
    buffer_point_range: Range<Point>,
    range_display: SharedString,
    style: HighlightStyle,
    category: HighlightCategory,
}

/// An item in the display list: either a separator between excerpts or a highlight entry.
#[derive(Debug, Clone)]
enum DisplayItem {
    ExcerptSeparator {
        label: SharedString,
    },
    Entry {
        /// Index into `cached_entries`.
        entry_ix: usize,
    },
}

pub struct HighlightsTreeView {
    workspace_handle: WeakEntity<Workspace>,
    editor: Option<EditorState>,
    list_scroll_handle: UniformListScrollHandle,
    selected_item_ix: Option<usize>,
    hovered_item_ix: Option<usize>,
    focus_handle: FocusHandle,
    cached_entries: Vec<HighlightEntry>,
    display_items: Vec<DisplayItem>,
    is_singleton: bool,
    show_text_highlights: bool,
    show_syntax_tokens: bool,
    show_semantic_tokens: bool,
    skip_next_scroll: bool,
    refresh_task: Task<()>,
}

pub struct HighlightsTreeToolbarItemView {
    tree_view: Option<Entity<HighlightsTreeView>>,
    _subscription: Option<gpui::Subscription>,
    toggle_settings_handle: PopoverMenuHandle<ContextMenu>,
}

struct EditorState {
    editor: Entity<Editor>,
    _subscription: gpui::Subscription,
}

struct SemanticHighlightEntry {
    range: Range<Anchor>,
    style: HighlightStyle,
    category: HighlightCategory,
}

struct HighlightRefreshInput {
    multi_buffer_snapshot: MultiBufferSnapshot,
    text_highlights: Vec<(HighlightKey, Arc<(HighlightStyle, Vec<Range<Anchor>>)>)>,
    semantic_highlights: Vec<SemanticHighlightEntry>,
    syntax_theme: Arc<SyntaxTheme>,
}

impl HighlightsTreeView {
    pub fn new(
        workspace_handle: WeakEntity<Workspace>,
        active_item: Option<Box<dyn ItemHandle>>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> Self {
        let mut this = Self {
            workspace_handle: workspace_handle.clone(),
            list_scroll_handle: UniformListScrollHandle::new(),
            editor: None,
            hovered_item_ix: None,
            selected_item_ix: None,
            focus_handle: cx.focus_handle(),
            cached_entries: Vec::new(),
            display_items: Vec::new(),
            is_singleton: true,
            show_text_highlights: true,
            show_syntax_tokens: true,
            show_semantic_tokens: true,
            skip_next_scroll: false,
            refresh_task: Task::ready(()),
        };

        this.handle_item_updated(active_item, window, cx);

        cx.subscribe_in(
            &workspace_handle.upgrade().unwrap(),
            window,
            move |this, workspace, event, window, cx| match event {
                WorkspaceEvent::ItemAdded { .. } | WorkspaceEvent::ActiveItemChanged => {
                    this.handle_item_updated(workspace.read(cx).active_item(cx), window, cx)
                }
                WorkspaceEvent::ItemRemoved { item_id } => {
                    this.handle_item_removed(item_id, window, cx);
                }
                _ => {}
            },
        )
        .detach();

        this
    }

    fn handle_item_updated(
        &mut self,
        active_item: Option<Box<dyn ItemHandle>>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let active_editor = match active_item {
            Some(active_item) => {
                if active_item.item_id() == cx.entity_id() {
                    return;
                } else {
                    match active_item.downcast::<Editor>() {
                        Some(active_editor) => active_editor,
                        None => {
                            self.clear(cx);
                            return;
                        }
                    }
                }
            }
            None => {
                self.clear(cx);
                return;
            }
        };

        let is_different_editor = self
            .editor
            .as_ref()
            .is_none_or(|state| state.editor != active_editor);
        if is_different_editor {
            self.set_editor(active_editor, window, cx);
        }
    }

    fn handle_item_removed(
        &mut self,
        item_id: &EntityId,
        _window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        if self
            .editor
            .as_ref()
            .is_some_and(|state| state.editor.entity_id() == *item_id)
        {
            self.clear(cx);
        }
    }

    fn clear(&mut self, cx: &mut Context<Self>) {
        self.refresh_task = Task::ready(());
        self.cached_entries.clear();
        self.display_items.clear();
        self.selected_item_ix = None;
        self.hovered_item_ix = None;
        if let Some(state) = self.editor.take() {
            Self::clear_editor_highlights(&state.editor, cx);
        }
        cx.notify();
    }

    fn set_editor(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) {
        if let Some(state) = &self.editor {
            if state.editor == editor {
                return;
            }
            Self::clear_editor_highlights(&state.editor, cx);
        }

        let subscription =
            cx.subscribe_in(&editor, window, |this, _, event, window, cx| match event {
                editor::EditorEvent::Reparsed(_) => this.schedule_refresh_highlights(window, cx),
                editor::EditorEvent::SelectionsChanged { .. } => {
                    this.update_selection_from_editor(cx);
                }
                _ => return,
            });

        self.editor = Some(EditorState {
            editor,
            _subscription: subscription,
        });
        self.refresh_task = Task::ready(());
        self.cached_entries.clear();
        self.display_items.clear();
        self.selected_item_ix = None;
        self.hovered_item_ix = None;
        self.schedule_refresh_highlights(window, cx);
        cx.notify();
    }

    fn schedule_refresh_highlights(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        self.refresh_task = cx.spawn_in(window, async move |this, cx| {
            cx.background_executor()
                .timer(Duration::from_millis(30))
                .await;

            let Some(input) = this
                .update(cx, |this, cx| this.highlight_refresh_input(cx))
                .ok()
                .flatten()
            else {
                return;
            };

            let new_highlights = cx
                .background_spawn(async move { build_highlight_entries(input) })
                .await;

            this.update_in(cx, |this, _window, cx| {
                this.apply_highlight_refresh(new_highlights, cx);
            })
            .ok();
        });
    }

    fn highlight_refresh_input(&self, cx: &mut Context<Self>) -> Option<HighlightRefreshInput> {
        let editor_state = self.editor.as_ref()?;
        let editor = editor_state.editor.read(cx);
        let display_map = editor.display_map.clone();
        let project = editor.project().cloned()?;
        let multi_buffer = editor.buffer().clone();
        let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
        let syntax_theme = cx.theme().syntax().clone();

        let (text_highlights, semantic_token_highlights) =
            display_map.update(cx, |display_map, _| {
                let text_highlights = display_map
                    .all_text_highlights()
                    .map(|(key, highlights)| (*key, highlights.clone()))
                    .collect::<Vec<_>>();
                let semantic_token_highlights = display_map
                    .all_semantic_token_highlights()
                    .map(|(buffer_id, (tokens, interner))| {
                        (*buffer_id, tokens.clone(), interner.clone())
                    })
                    .collect::<Vec<_>>();
                (text_highlights, semantic_token_highlights)
            });

        let mut semantic_highlights = Vec::new();
        project.read(cx).lsp_store().update(cx, |lsp_store, cx| {
            for (buffer_id, tokens, interner) in semantic_token_highlights {
                let language_name = multi_buffer
                    .read(cx)
                    .buffer(buffer_id)
                    .and_then(|buffer| buffer.read(cx).language().map(|language| language.name()));

                for token in tokens.iter() {
                    let Some(stylizer) = lsp_store.get_or_create_token_stylizer(
                        token.server_id,
                        language_name.as_ref(),
                        cx,
                    ) else {
                        continue;
                    };

                    let theme_key = stylizer
                        .rules_for_token(token.token_type)
                        .and_then(|rules| {
                            rules
                                .iter()
                                .filter(|rule| {
                                    rule.token_modifiers.iter().all(|modifier| {
                                        stylizer.has_modifier(token.token_modifiers, modifier)
                                    })
                                })
                                .fold(None, |theme_key, rule| {
                                    rule.style
                                        .iter()
                                        .find(|style_name| {
                                            syntax_theme.style_for_name(style_name).is_some()
                                        })
                                        .map(|style_name| SharedString::from(style_name.clone()))
                                        .or(theme_key)
                                })
                        });

                    semantic_highlights.push(SemanticHighlightEntry {
                        range: token.range.start..token.range.end,
                        style: interner[token.style],
                        category: HighlightCategory::SemanticToken {
                            token_type: stylizer.token_type_name(token.token_type).cloned(),
                            token_modifiers: stylizer
                                .token_modifiers(token.token_modifiers)
                                .map(SharedString::from),
                            theme_key,
                        },
                    });
                }
            }
        });

        Some(HighlightRefreshInput {
            multi_buffer_snapshot,
            text_highlights,
            semantic_highlights,
            syntax_theme,
        })
    }

    fn apply_highlight_refresh(
        &mut self,
        new_highlights: Vec<HighlightEntry>,
        cx: &mut Context<Self>,
    ) {
        let Some(editor) = self.editor.as_ref().map(|state| state.editor.clone()) else {
            return;
        };
        let editor = editor.read(cx);
        let cursor_position = editor.selections.newest_anchor().head();
        let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);

        self.is_singleton = multi_buffer_snapshot.is_singleton();
        self.cached_entries = new_highlights;
        self.hovered_item_ix = None;
        self.rebuild_display_items(&multi_buffer_snapshot, cx);

        let should_scroll = !mem::take(&mut self.skip_next_scroll);
        self.update_selection_for_cursor(&cursor_position, &multi_buffer_snapshot, should_scroll);
        self.sync_editor_highlight_for_selected_entry(cx);
        cx.notify();
    }

    fn rebuild_display_items(&mut self, snapshot: &MultiBufferSnapshot, cx: &App) {
        self.display_items.clear();

        let mut last_range_end = None;
        for (entry_ix, entry) in self.cached_entries.iter().enumerate() {
            if !self.should_show_entry(entry) {
                continue;
            }

            if !self.is_singleton {
                let excerpt_changed = last_range_end.is_none_or(|anchor| {
                    snapshot
                        .excerpt_containing(anchor..entry.range.start)
                        .is_none()
                });
                if excerpt_changed {
                    last_range_end = Some(entry.range.end);
                    let label = excerpt_label_for(entry, snapshot, cx);
                    self.display_items
                        .push(DisplayItem::ExcerptSeparator { label });
                }
            }

            self.display_items.push(DisplayItem::Entry { entry_ix });
        }
    }

    fn should_show_entry(&self, entry: &HighlightEntry) -> bool {
        match entry.category {
            HighlightCategory::Text(_) => self.show_text_highlights,
            HighlightCategory::SyntaxToken { .. } => self.show_syntax_tokens,
            HighlightCategory::SemanticToken { .. } => self.show_semantic_tokens,
        }
    }

    fn update_selection_from_editor(&mut self, cx: &mut Context<Self>) {
        let Some(editor_state) = self.editor.as_ref() else {
            return;
        };

        let (cursor_position, multi_buffer_snapshot) = {
            let editor = editor_state.editor.read(cx);
            (
                editor.selections.newest_anchor().head(),
                editor.buffer().read(cx).snapshot(cx),
            )
        };

        let should_update_selection = !mem::take(&mut self.skip_next_scroll);
        if !should_update_selection {
            self.sync_editor_highlight_for_selected_entry(cx);
            return;
        }

        let should_notify =
            self.update_selection_for_cursor(&cursor_position, &multi_buffer_snapshot, true);
        self.sync_editor_highlight_for_selected_entry(cx);
        if should_notify {
            cx.notify();
        }
    }

    fn update_selection_for_cursor(
        &mut self,
        cursor: &Anchor,
        snapshot: &MultiBufferSnapshot,
        should_scroll: bool,
    ) -> bool {
        let cursor_point = cursor.to_point(snapshot);
        let Some((cursor_buffer, cursor_point)) = snapshot.point_to_buffer_point(cursor_point)
        else {
            let changed = self.selected_item_ix.take().is_some();
            return changed;
        };
        let cursor_buffer_id = cursor_buffer.remote_id();

        let best = self
            .display_items
            .iter()
            .enumerate()
            .filter_map(|(display_ix, item)| match item {
                DisplayItem::Entry { entry_ix } => {
                    let entry = &self.cached_entries[*entry_ix];
                    Some((display_ix, *entry_ix, entry))
                }
                _ => None,
            })
            .filter(|(_, _, entry)| {
                entry.buffer_id == cursor_buffer_id
                    && entry.buffer_point_range.start <= cursor_point
                    && cursor_point < entry.buffer_point_range.end
            })
            .min_by_key(|(_, _, entry)| {
                (
                    entry
                        .buffer_point_range
                        .end
                        .row
                        .saturating_sub(entry.buffer_point_range.start.row),
                    entry
                        .buffer_point_range
                        .end
                        .column
                        .saturating_sub(entry.buffer_point_range.start.column),
                )
            })
            .map(|(display_ix, entry_ix, _)| (display_ix, entry_ix));

        let selected_item_ix = best.map(|(_, entry_ix)| entry_ix);
        let changed = self.selected_item_ix != selected_item_ix;
        self.selected_item_ix = selected_item_ix;

        if should_scroll && let Some((display_ix, _)) = best {
            self.list_scroll_handle
                .scroll_to_item(display_ix, ScrollStrategy::Center);
            return true;
        }

        changed
    }

    fn update_editor_with_range_for_entry(
        &self,
        entry_ix: usize,
        window: &mut Window,
        cx: &mut Context<Self>,
        f: &mut dyn FnMut(&mut Editor, Range<Anchor>, usize, &mut Window, &mut Context<Editor>),
    ) -> Option<()> {
        let editor_state = self.editor.as_ref()?;
        let entry = self.cached_entries.get(entry_ix)?;
        let range = entry.range.clone();
        let key = cx.entity_id().as_u64() as usize;

        editor_state.editor.update(cx, |editor, cx| {
            f(editor, range, key, window, cx);
        });
        Some(())
    }

    fn render_entry(&self, entry: &HighlightEntry, selected: bool, cx: &App) -> Div {
        let colors = cx.theme().colors();
        let style_preview = render_style_preview(entry.style, selected, cx);

        h_flex()
            .gap_1()
            .child(style_preview)
            .child(Label::new(entry.range_display.clone()).color(Color::Default))
            .child(
                Label::new(entry.category.label())
                    .size(LabelSize::Small)
                    .color(Color::Muted),
            )
            .text_bg(if selected {
                colors.element_selected
            } else {
                Hsla::default()
            })
            .pl(rems(0.5))
            .hover(|style| style.bg(colors.element_hover))
    }

    fn render_separator(&self, label: &SharedString, cx: &App) -> Div {
        let colors = cx.theme().colors();
        h_flex()
            .gap_1()
            .px(rems(0.5))
            .bg(colors.surface_background)
            .border_b_1()
            .border_color(colors.border_variant)
            .child(
                Label::new(label.clone())
                    .size(LabelSize::Small)
                    .color(Color::Muted),
            )
    }

    fn compute_items(
        &mut self,
        visible_range: Range<usize>,
        _window: &mut Window,
        cx: &mut Context<Self>,
    ) -> Vec<Div> {
        let mut items = Vec::new();

        for display_ix in visible_range {
            let Some(display_item) = self.display_items.get(display_ix) else {
                continue;
            };

            match display_item {
                DisplayItem::ExcerptSeparator { label } => {
                    items.push(self.render_separator(label, cx));
                }
                DisplayItem::Entry { entry_ix } => {
                    let entry_ix = *entry_ix;
                    let entry = &self.cached_entries[entry_ix];
                    let selected = Some(entry_ix) == self.selected_item_ix;
                    let rendered = self
                        .render_entry(entry, selected, cx)
                        .on_mouse_down(
                            MouseButton::Left,
                            cx.listener(move |tree_view, _: &MouseDownEvent, window, cx| {
                                tree_view.selected_item_ix = Some(entry_ix);
                                tree_view.skip_next_scroll = true;
                                tree_view.update_editor_with_range_for_entry(
                                    entry_ix,
                                    window,
                                    cx,
                                    &mut |editor, mut range, _, window, cx| {
                                        mem::swap(&mut range.start, &mut range.end);
                                        editor.change_selections(
                                            SelectionEffects::scroll(Autoscroll::newest()),
                                            window,
                                            cx,
                                            |selections| {
                                                selections.select_ranges([range]);
                                            },
                                        );
                                    },
                                );
                                cx.notify();
                            }),
                        )
                        .on_mouse_move(cx.listener(
                            move |tree_view, _: &MouseMoveEvent, window, cx| {
                                if tree_view.hovered_item_ix != Some(entry_ix) {
                                    tree_view.hovered_item_ix = Some(entry_ix);
                                    tree_view.update_editor_with_range_for_entry(
                                        entry_ix,
                                        window,
                                        cx,
                                        &mut |editor, range, key, _, cx| {
                                            Self::set_editor_highlights(editor, key, &[range], cx);
                                        },
                                    );
                                    cx.notify();
                                }
                            },
                        ));

                    items.push(rendered);
                }
            }
        }

        items
    }

    fn set_editor_highlights(
        editor: &mut Editor,
        key: usize,
        ranges: &[Range<Anchor>],
        cx: &mut Context<Editor>,
    ) {
        editor.highlight_background(
            HighlightKey::HighlightsTreeView(key),
            ranges,
            |_, theme| theme.colors().editor_document_highlight_write_background,
            cx,
        );
    }

    fn sync_editor_highlight_for_selected_entry(&self, cx: &mut Context<Self>) {
        let Some(editor_state) = self.editor.as_ref() else {
            return;
        };
        let key = cx.entity_id().as_u64() as usize;
        let range = self
            .selected_item_ix
            .and_then(|entry_ix| self.cached_entries.get(entry_ix))
            .map(|entry| entry.range.clone());

        editor_state.editor.update(cx, |editor, cx| {
            if let Some(range) = range {
                Self::set_editor_highlights(editor, key, &[range], cx);
            } else {
                editor.clear_background_highlights(HighlightKey::HighlightsTreeView(key), cx);
            }
        });
    }

    fn clear_editor_highlights(editor: &Entity<Editor>, cx: &mut Context<Self>) {
        let highlight_key = HighlightKey::HighlightsTreeView(cx.entity_id().as_u64() as usize);
        editor.update(cx, |editor, cx| {
            editor.clear_background_highlights(highlight_key, cx);
        });
    }

    fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
        self.move_selection(-1, window, cx);
    }

    fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
        self.move_selection(1, window, cx);
    }

    fn move_selection(&mut self, delta: i32, window: &mut Window, cx: &mut Context<Self>) {
        if self.display_items.is_empty() {
            return;
        }

        let entry_display_items: Vec<(usize, usize)> = self
            .display_items
            .iter()
            .enumerate()
            .filter_map(|(display_ix, item)| match item {
                DisplayItem::Entry { entry_ix } => Some((display_ix, *entry_ix)),
                _ => None,
            })
            .collect();

        if entry_display_items.is_empty() {
            return;
        }

        let current_pos = self
            .selected_item_ix
            .and_then(|selected| {
                entry_display_items
                    .iter()
                    .position(|(_, entry_ix)| *entry_ix == selected)
            })
            .unwrap_or(0);

        let new_pos = if delta < 0 {
            current_pos.saturating_sub((-delta) as usize)
        } else {
            (current_pos + delta as usize).min(entry_display_items.len() - 1)
        };

        if let Some(&(display_ix, entry_ix)) = entry_display_items.get(new_pos) {
            self.selected_item_ix = Some(entry_ix);
            self.skip_next_scroll = true;
            self.list_scroll_handle
                .scroll_to_item(display_ix, ScrollStrategy::Center);

            self.update_editor_with_range_for_entry(
                entry_ix,
                window,
                cx,
                &mut |editor, mut range, _, window, cx| {
                    mem::swap(&mut range.start, &mut range.end);
                    editor.change_selections(
                        SelectionEffects::scroll(Autoscroll::newest()),
                        window,
                        cx,
                        |selections| {
                            selections.select_ranges([range]);
                        },
                    );
                },
            );

            cx.notify();
        }
    }

    fn entry_count(&self) -> usize {
        self.cached_entries
            .iter()
            .filter(|entry| self.should_show_entry(entry))
            .count()
    }
}

impl Render for HighlightsTreeView {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let display_count = self.display_items.len();

        div()
            .flex_1()
            .track_focus(&self.focus_handle)
            .key_context("HighlightsTreeView")
            .on_action(cx.listener(Self::select_previous))
            .on_action(cx.listener(Self::select_next))
            .bg(cx.theme().colors().editor_background)
            .map(|this| {
                if display_count > 0 {
                    this.child(
                        uniform_list(
                            "HighlightsTreeView",
                            display_count,
                            cx.processor(move |this, range: Range<usize>, window, cx| {
                                this.compute_items(range, window, cx)
                            }),
                        )
                        .size_full()
                        .track_scroll(&self.list_scroll_handle)
                        .text_bg(cx.theme().colors().background)
                        .into_any_element(),
                    )
                    .vertical_scrollbar_for(&self.list_scroll_handle, window, cx)
                    .into_any_element()
                } else {
                    let inner_content = v_flex()
                        .items_center()
                        .text_center()
                        .gap_2()
                        .max_w_3_5()
                        .map(|this| {
                            if self.editor.is_some() {
                                let has_any = !self.cached_entries.is_empty();
                                if has_any {
                                    this.child(Label::new("All highlights are filtered out"))
                                        .child(
                                            Label::new(
                                                "Enable text, syntax, or semantic highlights in the toolbar",
                                            )
                                            .size(LabelSize::Small),
                                        )
                                } else {
                                    this.child(Label::new("No highlights found")).child(
                                        Label::new(
                                            "The editor has no text, syntax, or semantic token highlights",
                                        )
                                        .size(LabelSize::Small),
                                    )
                                }
                            } else {
                                this.child(Label::new("Not attached to an editor")).child(
                                    Label::new("Focus an editor to show highlights")
                                        .size(LabelSize::Small),
                                )
                            }
                        });

                    this.h_flex()
                        .size_full()
                        .justify_center()
                        .child(inner_content)
                        .into_any_element()
                }
            })
    }
}

impl EventEmitter<()> for HighlightsTreeView {}

impl Focusable for HighlightsTreeView {
    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
        self.focus_handle.clone()
    }
}

impl Item for HighlightsTreeView {
    type Event = ();

    fn to_item_events(_: &Self::Event, _: &mut dyn FnMut(workspace::item::ItemEvent)) {}

    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
        "Highlights".into()
    }

    fn telemetry_event_text(&self) -> Option<&'static str> {
        None
    }

    fn can_split(&self) -> bool {
        true
    }

    fn clone_on_split(
        &self,
        _: Option<workspace::WorkspaceId>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> Task<Option<Entity<Self>>>
    where
        Self: Sized,
    {
        Task::ready(Some(cx.new(|cx| {
            let mut clone = Self::new(self.workspace_handle.clone(), None, window, cx);
            clone.show_text_highlights = self.show_text_highlights;
            clone.show_syntax_tokens = self.show_syntax_tokens;
            clone.show_semantic_tokens = self.show_semantic_tokens;
            clone.skip_next_scroll = false;
            if let Some(editor) = &self.editor {
                clone.set_editor(editor.editor.clone(), window, cx)
            }
            clone
        })))
    }

    fn on_removed(&self, cx: &mut Context<Self>) {
        if let Some(state) = self.editor.as_ref() {
            Self::clear_editor_highlights(&state.editor, cx);
        }
    }
}

impl Default for HighlightsTreeToolbarItemView {
    fn default() -> Self {
        Self::new()
    }
}

impl HighlightsTreeToolbarItemView {
    pub fn new() -> Self {
        Self {
            tree_view: None,
            _subscription: None,
            toggle_settings_handle: PopoverMenuHandle::default(),
        }
    }

    fn render_header(&self, cx: &Context<Self>) -> Option<ButtonLike> {
        let tree_view = self.tree_view.as_ref()?;
        let tree_view = tree_view.read(cx);

        let total = tree_view.cached_entries.len();
        let filtered = tree_view.entry_count();

        let label = if filtered == total {
            format!("{} highlights", total)
        } else {
            format!("{} / {} highlights", filtered, total)
        };

        Some(ButtonLike::new("highlights header").child(Label::new(label)))
    }

    fn render_settings_button(&self, cx: &Context<Self>) -> PopoverMenu<ContextMenu> {
        let (show_text, show_syntax, show_semantic) = self
            .tree_view
            .as_ref()
            .map(|view| {
                let v = view.read(cx);
                (
                    v.show_text_highlights,
                    v.show_syntax_tokens,
                    v.show_semantic_tokens,
                )
            })
            .unwrap_or((true, true, true));

        let tree_view = self.tree_view.as_ref().map(|v| v.downgrade());

        PopoverMenu::new("highlights-tree-settings")
            .trigger_with_tooltip(
                IconButton::new("toggle-highlights-settings-icon", IconName::Sliders)
                    .icon_size(IconSize::Small)
                    .style(ButtonStyle::Subtle)
                    .toggle_state(self.toggle_settings_handle.is_deployed()),
                Tooltip::text("Highlights Settings"),
            )
            .anchor(gpui::Anchor::TopRight)
            .with_handle(self.toggle_settings_handle.clone())
            .menu(move |window, cx| {
                let tree_view_for_text = tree_view.clone();
                let tree_view_for_syntax = tree_view.clone();
                let tree_view_for_semantic = tree_view.clone();

                let menu = ContextMenu::build(window, cx, move |menu, _, _| {
                    menu.toggleable_entry(
                        "Text Highlights",
                        show_text,
                        IconPosition::Start,
                        Some(ToggleTextHighlights.boxed_clone()),
                        {
                            let tree_view = tree_view_for_text.clone();
                            move |_, cx| {
                                if let Some(view) = tree_view.as_ref() {
                                    view.update(cx, |view, cx| {
                                        view.show_text_highlights = !view.show_text_highlights;
                                        let snapshot = view.editor.as_ref().map(|s| {
                                            s.editor.read(cx).buffer().read(cx).snapshot(cx)
                                        });
                                        if let Some(snapshot) = snapshot {
                                            view.rebuild_display_items(&snapshot, cx);
                                        }
                                        cx.notify();
                                    })
                                    .ok();
                                }
                            }
                        },
                    )
                    .toggleable_entry(
                        "Syntax Tokens",
                        show_syntax,
                        IconPosition::Start,
                        Some(ToggleSyntaxTokens.boxed_clone()),
                        {
                            let tree_view = tree_view_for_syntax.clone();
                            move |_, cx| {
                                if let Some(view) = tree_view.as_ref() {
                                    view.update(cx, |view, cx| {
                                        view.show_syntax_tokens = !view.show_syntax_tokens;
                                        let snapshot = view.editor.as_ref().map(|s| {
                                            s.editor.read(cx).buffer().read(cx).snapshot(cx)
                                        });
                                        if let Some(snapshot) = snapshot {
                                            view.rebuild_display_items(&snapshot, cx);
                                        }
                                        cx.notify();
                                    })
                                    .ok();
                                }
                            }
                        },
                    )
                    .toggleable_entry(
                        "Semantic Tokens",
                        show_semantic,
                        IconPosition::Start,
                        Some(ToggleSemanticTokens.boxed_clone()),
                        {
                            move |_, cx| {
                                if let Some(view) = tree_view_for_semantic.as_ref() {
                                    view.update(cx, |view, cx| {
                                        view.show_semantic_tokens = !view.show_semantic_tokens;
                                        let snapshot = view.editor.as_ref().map(|s| {
                                            s.editor.read(cx).buffer().read(cx).snapshot(cx)
                                        });
                                        if let Some(snapshot) = snapshot {
                                            view.rebuild_display_items(&snapshot, cx);
                                        }
                                        cx.notify();
                                    })
                                    .ok();
                                }
                            }
                        },
                    )
                });

                Some(menu)
            })
    }
}

impl Render for HighlightsTreeToolbarItemView {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        h_flex()
            .gap_1()
            .children(self.render_header(cx))
            .child(self.render_settings_button(cx))
    }
}

impl EventEmitter<ToolbarItemEvent> for HighlightsTreeToolbarItemView {}

impl ToolbarItemView for HighlightsTreeToolbarItemView {
    fn set_active_pane_item(
        &mut self,
        active_pane_item: Option<&dyn ItemHandle>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> ToolbarItemLocation {
        if let Some(item) = active_pane_item
            && let Some(view) = item.downcast::<HighlightsTreeView>()
        {
            self.tree_view = Some(view.clone());
            self._subscription = Some(cx.observe_in(&view, window, |_, _, _, cx| cx.notify()));
            return ToolbarItemLocation::PrimaryLeft;
        }
        self.tree_view = None;
        self._subscription = None;
        ToolbarItemLocation::Hidden
    }
}

fn excerpt_label_for(
    entry: &HighlightEntry,
    snapshot: &MultiBufferSnapshot,
    cx: &App,
) -> SharedString {
    let path_label = snapshot
        .anchor_to_buffer_anchor(entry.range.start)
        .and_then(|(anchor, _)| snapshot.buffer_for_id(anchor.buffer_id))
        .and_then(|buf| buf.file())
        .map(|file| {
            let full_path = file.full_path(cx);
            full_path.to_string_lossy().to_string()
        })
        .unwrap_or_else(|| "untitled".to_string());
    path_label.into()
}

fn build_highlight_entries(
    HighlightRefreshInput {
        multi_buffer_snapshot,
        text_highlights,
        semantic_highlights,
        syntax_theme,
    }: HighlightRefreshInput,
) -> Vec<HighlightEntry> {
    let mut entries = Vec::new();

    for (key, text_highlights) in text_highlights {
        for range in &text_highlights.1 {
            let Some((range_display, buffer_id, buffer_point_range)) =
                format_anchor_range(range, &multi_buffer_snapshot)
            else {
                continue;
            };
            entries.push(HighlightEntry {
                range: range.clone(),
                buffer_id,
                range_display,
                style: text_highlights.0,
                category: HighlightCategory::Text(key),
                buffer_point_range,
            });
        }
    }

    for highlight in semantic_highlights {
        let Some((range_display, buffer_id, buffer_point_range)) =
            format_anchor_range(&highlight.range, &multi_buffer_snapshot)
        else {
            continue;
        };

        entries.push(HighlightEntry {
            range: highlight.range,
            buffer_id,
            range_display,
            style: highlight.style,
            category: highlight.category,
            buffer_point_range,
        });
    }

    for excerpt_range in multi_buffer_snapshot.excerpts() {
        let Some(buffer_snapshot) =
            multi_buffer_snapshot.buffer_for_id(excerpt_range.context.start.buffer_id)
        else {
            continue;
        };

        let start_offset = excerpt_range.context.start.to_offset(buffer_snapshot);
        let end_offset = excerpt_range.context.end.to_offset(buffer_snapshot);
        let range = start_offset..end_offset;

        let captures = buffer_snapshot.captures(range, |grammar| {
            grammar
                .highlights_config
                .as_ref()
                .map(|config| &config.query)
        });
        let grammars = captures.grammars().to_vec();
        let highlight_maps = grammars
            .iter()
            .map(|grammar| grammar.highlight_map())
            .collect::<Vec<_>>();

        for capture in captures {
            let Some(highlight_id) = highlight_maps[capture.grammar_index].get(capture.index)
            else {
                continue;
            };
            let Some(style) = syntax_theme.get(highlight_id).cloned() else {
                continue;
            };

            let theme_key = syntax_theme
                .get_capture_name(highlight_id)
                .map(|theme_key| SharedString::from(theme_key.to_string()));

            let capture_name = grammars[capture.grammar_index]
                .highlights_config
                .as_ref()
                .and_then(|config| config.query.capture_names().get(capture.index as usize))
                .map(|capture_name| SharedString::from((*capture_name).to_string()))
                .unwrap_or_else(|| SharedString::from("unknown"));

            let start_anchor = buffer_snapshot.anchor_before(capture.node.start_byte());
            let end_anchor = buffer_snapshot.anchor_after(capture.node.end_byte());

            let start = multi_buffer_snapshot.anchor_in_excerpt(start_anchor);
            let end = multi_buffer_snapshot.anchor_in_excerpt(end_anchor);

            let (start, end) = match (start, end) {
                (Some(start), Some(end)) => (start, end),
                _ => continue,
            };

            let range = start..end;
            let Some((range_display, buffer_id, buffer_point_range)) =
                format_anchor_range(&range, &multi_buffer_snapshot)
            else {
                continue;
            };

            entries.push(HighlightEntry {
                range,
                buffer_id,
                range_display,
                style,
                category: HighlightCategory::SyntaxToken {
                    capture_name,
                    theme_key,
                },
                buffer_point_range,
            });
        }
    }

    entries.sort_by(|a, b| {
        a.buffer_id
            .cmp(&b.buffer_id)
            .then_with(|| a.buffer_point_range.start.cmp(&b.buffer_point_range.start))
            .then_with(|| a.buffer_point_range.end.cmp(&b.buffer_point_range.end))
            .then_with(|| a.category.cmp(&b.category))
    });
    entries.dedup_by(|a, b| {
        a.buffer_id == b.buffer_id
            && a.buffer_point_range == b.buffer_point_range
            && a.category == b.category
    });

    entries
}

fn format_anchor_range(
    range: &Range<Anchor>,
    snapshot: &MultiBufferSnapshot,
) -> Option<(SharedString, BufferId, Range<Point>)> {
    let start = range.start.to_point(snapshot);
    let end = range.end.to_point(snapshot);
    let ((start_buffer, start), (_, end)) = snapshot
        .point_to_buffer_point(start)
        .zip(snapshot.point_to_buffer_point(end))?;
    let display = SharedString::from(format!(
        "[{}:{} - {}:{}]",
        start.row + 1,
        start.column + 1,
        end.row + 1,
        end.column + 1,
    ));
    Some((display, start_buffer.remote_id(), start..end))
}

fn render_style_preview(style: HighlightStyle, selected: bool, cx: &App) -> Div {
    let colors = cx.theme().colors();

    let display_color = style.color.or(style.background_color);

    let mut preview = div().px_1().rounded_sm();

    if let Some(color) = display_color {
        if selected {
            preview = preview.border_1().border_color(color).text_color(color);
        } else {
            preview = preview.bg(color);
        }
    } else {
        preview = preview.bg(colors.element_background);
    }

    let mut parts = Vec::new();

    if let Some(color) = display_color {
        parts.push(format_hsla_as_hex(color));
    }
    if style.font_weight.is_some() {
        parts.push("bold".to_string());
    }
    if style.font_style.is_some() {
        parts.push("italic".to_string());
    }
    if style.strikethrough.is_some() {
        parts.push("strike".to_string());
    }
    if style.underline.is_some() {
        parts.push("underline".to_string());
    }

    let label_text = if parts.is_empty() {
        "none".to_string()
    } else {
        parts.join(" ")
    };

    preview.child(Label::new(label_text).size(LabelSize::Small).when_some(
        display_color.filter(|_| selected),
        |label, display_color| label.color(Color::Custom(display_color)),
    ))
}

fn format_hsla_as_hex(color: Hsla) -> String {
    let rgba = color.to_rgb();
    let r = (rgba.r * 255.0).round() as u8;
    let g = (rgba.g * 255.0).round() as u8;
    let b = (rgba.b * 255.0).round() as u8;
    let a = (rgba.a * 255.0).round() as u8;
    if a == 255 {
        format!("#{:02X}{:02X}{:02X}", r, g, b)
    } else {
        format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a)
    }
}
