use anyhow::anyhow;
use dap::Module;
use gpui::{
    AnyElement, Entity, FocusHandle, Focusable, ScrollStrategy, Subscription, Task,
    UniformListScrollHandle, WeakEntity, uniform_list,
};
use project::{
    ProjectItem as _, ProjectPath,
    debugger::session::{Session, SessionEvent},
};
use std::{ops::Range, path::Path, sync::Arc};
use ui::{WithScrollbar, prelude::*};
use workspace::Workspace;

pub struct ModuleList {
    scroll_handle: UniformListScrollHandle,
    selected_ix: Option<usize>,
    session: Entity<Session>,
    workspace: WeakEntity<Workspace>,
    focus_handle: FocusHandle,
    entries: Vec<Module>,
    _rebuild_task: Option<Task<()>>,
    _subscription: Subscription,
}

impl ModuleList {
    pub fn new(
        session: Entity<Session>,
        workspace: WeakEntity<Workspace>,
        cx: &mut Context<Self>,
    ) -> Self {
        let focus_handle = cx.focus_handle();

        let _subscription = cx.subscribe(&session, |this, _, event, cx| match event {
            SessionEvent::Stopped(_)
            | SessionEvent::HistoricSnapshotSelected
            | SessionEvent::Modules => {
                if this._rebuild_task.is_some() {
                    this.schedule_rebuild(cx);
                }
            }
            _ => {}
        });

        let scroll_handle = UniformListScrollHandle::new();

        Self {
            scroll_handle,
            session,
            workspace,
            focus_handle,
            entries: Vec::new(),
            selected_ix: None,
            _subscription,
            _rebuild_task: None,
        }
    }

    fn schedule_rebuild(&mut self, cx: &mut Context<Self>) {
        self._rebuild_task = Some(cx.spawn(async move |this, cx| {
            this.update(cx, |this, cx| {
                let modules = this
                    .session
                    .update(cx, |session, cx| session.modules(cx).to_owned());
                this.entries = modules;
                cx.notify();
            })
            .ok();
        }));
    }

    fn open_module(&mut self, path: Arc<Path>, window: &mut Window, cx: &mut Context<Self>) {
        cx.spawn_in(window, async move |this, cx| {
            let (worktree, relative_path) = this
                .update(cx, |this, cx| {
                    this.workspace.update(cx, |workspace, cx| {
                        workspace.project().update(cx, |this, cx| {
                            this.find_or_create_worktree(&path, false, cx)
                        })
                    })
                })??
                .await?;

            let buffer = this
                .update(cx, |this, cx| {
                    this.workspace.update(cx, |this, cx| {
                        this.project().update(cx, |this, cx| {
                            let worktree_id = worktree.read(cx).id();
                            this.open_buffer(
                                ProjectPath {
                                    worktree_id,
                                    path: relative_path,
                                },
                                cx,
                            )
                        })
                    })
                })??
                .await?;

            this.update_in(cx, |this, window, cx| {
                this.workspace.update(cx, |workspace, cx| {
                    let project_path = buffer.read(cx).project_path(cx).ok_or_else(|| {
                        anyhow!("Could not select a stack frame for unnamed buffer")
                    })?;
                    anyhow::Ok(workspace.open_path_preview(
                        project_path,
                        None,
                        false,
                        true,
                        true,
                        window,
                        cx,
                    ))
                })
            })???
            .await?;

            anyhow::Ok(())
        })
        .detach();
    }

    fn render_entry(&mut self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
        let module = self.entries[ix].clone();

        v_flex()
            .rounded_md()
            .w_full()
            .group("")
            .id(("module-list", ix))
            .on_any_mouse_down(|_, _, cx| {
                cx.stop_propagation();
            })
            .when(module.path.is_some(), |this| {
                this.on_click({
                    let path = module
                        .path
                        .as_deref()
                        .map(|path| Arc::<Path>::from(Path::new(path)));
                    cx.listener(move |this, _, window, cx| {
                        this.selected_ix = Some(ix);
                        if let Some(path) = path.as_ref() {
                            this.open_module(path.clone(), window, cx);
                        }
                        cx.notify();
                    })
                })
            })
            .p_1()
            .hover(|s| s.bg(cx.theme().colors().element_hover))
            .when(Some(ix) == self.selected_ix, |s| {
                s.bg(cx.theme().colors().element_hover)
            })
            .child(h_flex().gap_0p5().text_ui_sm(cx).child(module.name.clone()))
            .child(
                h_flex()
                    .text_ui_xs(cx)
                    .text_color(cx.theme().colors().text_muted)
                    .when_some(module.path, |this, path| this.child(path)),
            )
            .into_any()
    }

    #[cfg(test)]
    pub(crate) fn modules(&self, cx: &mut Context<Self>) -> Vec<dap::Module> {
        self.session
            .update(cx, |session, cx| session.modules(cx).to_vec())
    }

    fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
        let Some(ix) = self.selected_ix else { return };
        let Some(entry) = self.entries.get(ix) else {
            return;
        };
        let Some(path) = entry.path.as_deref() else {
            return;
        };
        let path = Arc::from(Path::new(path));
        self.open_module(path, window, cx);
    }

    fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
        self.selected_ix = ix;
        if let Some(ix) = ix {
            self.scroll_handle
                .scroll_to_item(ix, ScrollStrategy::Center);
        }
        cx.notify();
    }

    fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
        let ix = match self.selected_ix {
            _ if self.entries.is_empty() => None,
            None => Some(0),
            Some(ix) => {
                if ix == self.entries.len() - 1 {
                    Some(0)
                } else {
                    Some(ix + 1)
                }
            }
        };
        self.select_ix(ix, cx);
    }

    fn select_previous(
        &mut self,
        _: &menu::SelectPrevious,
        _window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let ix = match self.selected_ix {
            _ if self.entries.is_empty() => None,
            None => Some(self.entries.len() - 1),
            Some(ix) => {
                if ix == 0 {
                    Some(self.entries.len() - 1)
                } else {
                    Some(ix - 1)
                }
            }
        };
        self.select_ix(ix, cx);
    }

    fn select_first(
        &mut self,
        _: &menu::SelectFirst,
        _window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let ix = if !self.entries.is_empty() {
            Some(0)
        } else {
            None
        };
        self.select_ix(ix, cx);
    }

    fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
        let ix = if !self.entries.is_empty() {
            Some(self.entries.len() - 1)
        } else {
            None
        };
        self.select_ix(ix, cx);
    }

    fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        uniform_list(
            "module-list",
            self.entries.len(),
            cx.processor(|this, range: Range<usize>, _window, cx| {
                range.map(|ix| this.render_entry(ix, cx)).collect()
            }),
        )
        .track_scroll(&self.scroll_handle)
        .size_full()
    }
}

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

impl Render for ModuleList {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        if self._rebuild_task.is_none() {
            self.schedule_rebuild(cx);
        }
        div()
            .track_focus(&self.focus_handle)
            .on_action(cx.listener(Self::select_last))
            .on_action(cx.listener(Self::select_first))
            .on_action(cx.listener(Self::select_next))
            .on_action(cx.listener(Self::select_previous))
            .on_action(cx.listener(Self::confirm))
            .size_full()
            .p_1()
            .child(self.render_list(window, cx))
            .vertical_scrollbar_for(&self.scroll_handle, window, cx)
    }
}
