use gpui::{
    Action, App, AppContext as _, Entity, EventEmitter, FocusHandle, Focusable,
    KeyBindingContextPredicate, KeyContext, Keystroke, MouseButton, Render, Subscription, Task,
    actions,
};
use itertools::Itertools;
use serde_json::json;
use ui::{Button, ButtonStyle};
use ui::{
    ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement, Label, LabelCommon,
    LabelSize, ParentElement, SharedString, StatefulInteractiveElement, Styled, Window, div,
    h_flex, px, v_flex,
};
use workspace::{Item, SplitDirection, Workspace};

actions!(
    dev,
    [
        /// Opens the key context view for debugging keybindings.
        OpenKeyContextView
    ]
);

pub fn init(cx: &mut App) {
    cx.observe_new(|workspace: &mut Workspace, _, _| {
        workspace.register_action(|workspace, _: &OpenKeyContextView, window, cx| {
            let key_context_view = cx.new(|cx| KeyContextView::new(window, cx));
            workspace.split_item(
                SplitDirection::Right,
                Box::new(key_context_view),
                window,
                cx,
            )
        });
    })
    .detach();
}

struct KeyContextView {
    pending_keystrokes: Option<Vec<Keystroke>>,
    last_keystrokes: Option<SharedString>,
    last_possibilities: Vec<(SharedString, SharedString, Option<bool>)>,
    context_stack: Vec<KeyContext>,
    focus_handle: FocusHandle,
    _subscriptions: [Subscription; 2],
}

impl KeyContextView {
    pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
        let sub1 = cx.observe_keystrokes(|this, e, _, cx| {
            let mut pending = this.pending_keystrokes.take().unwrap_or_default();
            pending.push(e.keystroke.clone());
            let mut possibilities = cx.all_bindings_for_input(&pending);
            possibilities.reverse();
            this.last_keystrokes = Some(
                json!(pending.iter().map(|p| p.unparse()).join(" "))
                    .to_string()
                    .into(),
            );
            this.context_stack = e.context_stack.clone();
            this.last_possibilities = possibilities
                .into_iter()
                .map(|binding| {
                    let match_state = if let Some(predicate) = binding.predicate() {
                        if this.matches(&predicate) {
                            if this.action_matches(&e.action, binding.action()) {
                                Some(true)
                            } else {
                                Some(false)
                            }
                        } else {
                            None
                        }
                    } else if this.action_matches(&e.action, binding.action()) {
                        Some(true)
                    } else {
                        Some(false)
                    };
                    let predicate = if let Some(predicate) = binding.predicate() {
                        format!("{}", predicate)
                    } else {
                        "".to_string()
                    };
                    let mut name = binding.action().name();
                    if name == "zed::NoAction" {
                        name = "(null)"
                    }

                    (
                        name.to_owned().into(),
                        json!(predicate).to_string().into(),
                        match_state,
                    )
                })
                .collect();
            cx.notify();
        });
        let sub2 = cx.observe_pending_input(window, |this, window, cx| {
            this.pending_keystrokes = window.pending_input_keystrokes().map(|k| k.to_vec());
            if this.pending_keystrokes.is_some() {
                this.last_keystrokes.take();
            }
            cx.notify();
        });

        Self {
            context_stack: Vec::new(),
            pending_keystrokes: None,
            last_keystrokes: None,
            last_possibilities: Vec::new(),
            focus_handle: cx.focus_handle(),
            _subscriptions: [sub1, sub2],
        }
    }
}

impl EventEmitter<()> for KeyContextView {}

impl Focusable for KeyContextView {
    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
        self.focus_handle.clone()
    }
}
impl KeyContextView {
    fn set_context_stack(&mut self, stack: Vec<KeyContext>, cx: &mut Context<Self>) {
        self.context_stack = stack;
        cx.notify()
    }

    fn matches(&self, predicate: &KeyBindingContextPredicate) -> bool {
        predicate.depth_of(&self.context_stack).is_some()
    }

    fn action_matches(&self, a: &Option<Box<dyn Action>>, b: &dyn Action) -> bool {
        if let Some(last_action) = a {
            last_action.partial_eq(b)
        } else {
            b.name() == "zed::NoAction"
        }
    }
}

impl Item for KeyContextView {
    type Event = ();

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

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

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

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

    fn clone_on_split(
        &self,
        _workspace_id: Option<workspace::WorkspaceId>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> Task<Option<Entity<Self>>>
    where
        Self: Sized,
    {
        Task::ready(Some(cx.new(|cx| KeyContextView::new(window, cx))))
    }
}

impl Render for KeyContextView {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
        use itertools::Itertools;

        let key_equivalents = cx.keyboard_mapper().get_key_equivalents();
        v_flex()
            .id("key-context-view")
            .overflow_scroll()
            .size_full()
            .max_h_full()
            .pt_4()
            .pl_4()
            .track_focus(&self.focus_handle)
            .key_context("KeyContextView")
            .on_mouse_up_out(
                MouseButton::Left,
                cx.listener(|this, _, window, cx| {
                    this.last_keystrokes.take();
                    this.set_context_stack(window.context_stack(), cx);
                }),
            )
            .on_mouse_up_out(
                MouseButton::Right,
                cx.listener(|_, _, window, cx| {
                    cx.defer_in(window, |this, window, cx| {
                        this.last_keystrokes.take();
                        this.set_context_stack(window.context_stack(), cx);
                    });
                }),
            )
            .child(Label::new("Keyboard Context").size(LabelSize::Large))
            .child(Label::new("This view lets you determine the current context stack for creating custom key bindings in Zed. When a keyboard shortcut is triggered, it also shows all the possible contexts it could have triggered in, and which one matched."))
            .child(
                h_flex()
                    .mt_4()
                    .gap_4()
                    .child(
                        Button::new("open_documentation", "Open Documentation")
                            .style(ButtonStyle::Filled)
                            .on_click(|_, _, cx| cx.open_url("https://zed.dev/docs/key-bindings")),
                    )
                    .child(
                        Button::new("view_default_keymap", "View Default Keymap")
                            .style(ButtonStyle::Filled)
                            .key_binding(ui::KeyBinding::for_action(
                                &zed_actions::OpenDefaultKeymap,
                                cx
                            ))
                            .on_click(|_, window, cx| {
                                window.dispatch_action(zed_actions::OpenDefaultKeymap.boxed_clone(), cx);
                            }),
                    )
                    .child(
                        Button::new("edit_your_keymap", "Edit Keymap File")
                            .style(ButtonStyle::Filled)
                            .key_binding(ui::KeyBinding::for_action(&zed_actions::OpenKeymapFile, cx))
                            .on_click(|_, window, cx| {
                                window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx);
                            }),
                    ),
            )
            .child(
                Label::new("Current Context Stack")
                    .size(LabelSize::Large)
                    .mt_8(),
            )
            .children({
                self.context_stack.iter().enumerate().map(|(i, context)| {
                    let primary = context.primary().map(|e| e.key.clone()).unwrap_or_default();
                    let secondary = context
                        .secondary()
                        .map(|e| {
                            if let Some(value) = e.value.as_ref() {
                                format!("{}={}", e.key, value)
                            } else {
                                e.key.to_string()
                            }
                        })
                        .join(" ");
                    Label::new(format!("{} {}", primary, secondary)).ml(px(12. * (i + 1) as f32))
                })
            })
            .child(Label::new("Last Keystroke").mt_4().size(LabelSize::Large))
            .when_some(self.pending_keystrokes.as_ref(), |el, keystrokes| {
                el.child(
                    Label::new(format!(
                        "Waiting for more input: {}",
                        keystrokes.iter().map(|k| k.unparse()).join(" ")
                    ))
                    .ml(px(12.)),
                )
            })
            .when_some(self.last_keystrokes.as_ref(), |el, keystrokes| {
                el.child(Label::new(format!("Typed: {}", keystrokes)).ml_4())
                    .children(
                        self.last_possibilities
                            .iter()
                            .map(|(name, predicate, state)| {
                                let (text, color) = match state {
                                    Some(true) => ("(match)", ui::Color::Success),
                                    Some(false) => ("(low precedence)", ui::Color::Hint),
                                    None => ("(no match)", ui::Color::Error),
                                };
                                h_flex()
                                    .gap_2()
                                    .ml_8()
                                    .child(div().min_w(px(200.)).child(Label::new(name.clone())))
                                    .child(Label::new(predicate.clone()))
                                    .child(Label::new(text).color(color))
                            }),
                    )
            })
            .when_some(key_equivalents, |el, key_equivalents| {
                el.child(Label::new("Key Equivalents").mt_4().size(LabelSize::Large))
                    .child(Label::new("Shortcuts defined using some characters have been remapped so that shortcuts can be typed without holding option."))
                    .children(
                        key_equivalents
                            .iter()
                            .sorted()
                            .map(|(key, equivalent)| {
                                Label::new(format!("cmd-{} => cmd-{}", key, equivalent)).ml_8()
                            }),
                    )
            })
    }
}
