use core::fmt;
use core::num::NonZeroU32;
use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};

use crate::input::{KeyCode, KeyEvent, ModifierMask};

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct KeyChord {
    pub key: KeyCode,
    pub modifiers: ModifierMask,
}

impl KeyChord {
    #[must_use]
    pub const fn new(key: KeyCode, modifiers: ModifierMask) -> Self {
        Self { key, modifiers }
    }
}

impl From<KeyEvent> for KeyChord {
    fn from(event: KeyEvent) -> Self {
        Self {
            key: event.code,
            modifiers: event.modifiers,
        }
    }
}

impl fmt::Display for KeyChord {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Display::fmt(&self.modifiers, f)?;
        fmt::Display::fmt(&self.key, f)
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub enum HotkeyScope {
    Global,
    Viewport,
    FeatureTree,
    Sketch,
    Extrude,
    Modal,
    TextInput,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ActionId(NonZeroU32);

impl ActionId {
    #[must_use]
    pub const fn new(id: NonZeroU32) -> Self {
        Self(id)
    }

    #[must_use]
    pub const fn get(self) -> NonZeroU32 {
        self.0
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct HotkeyBinding {
    pub chord: KeyChord,
    pub scope: HotkeyScope,
    pub action: ActionId,
}

impl HotkeyBinding {
    #[must_use]
    pub const fn new(chord: KeyChord, scope: HotkeyScope, action: ActionId) -> Self {
        Self {
            chord,
            scope,
            action,
        }
    }
}

#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum HotkeyTableError {
    #[error("hotkey conflict: chord already bound in this scope")]
    Conflict {
        chord: KeyChord,
        scope: HotkeyScope,
        existing: ActionId,
        attempted: ActionId,
    },
}

#[derive(Clone, Debug, Default, PartialEq)]
pub struct HotkeyTable {
    bindings: BTreeMap<(HotkeyScope, KeyChord), ActionId>,
}

impl HotkeyTable {
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    pub fn try_from_bindings(bindings: Vec<HotkeyBinding>) -> Result<Self, HotkeyTableError> {
        bindings
            .into_iter()
            .try_fold(Self::new(), |mut table, binding| {
                table.try_register(binding)?;
                Ok(table)
            })
    }

    pub fn try_register(&mut self, binding: HotkeyBinding) -> Result<(), HotkeyTableError> {
        let key = (binding.scope, binding.chord);
        if let Some(&existing) = self.bindings.get(&key) {
            return Err(HotkeyTableError::Conflict {
                chord: binding.chord,
                scope: binding.scope,
                existing,
                attempted: binding.action,
            });
        }
        self.bindings.insert(key, binding.action);
        Ok(())
    }

    #[must_use]
    pub fn dispatch(&self, chord: KeyChord, scopes: &HotkeyScopes) -> Option<ActionId> {
        scopes
            .innermost_first()
            .find_map(|scope| self.bindings.get(&(*scope, chord)).copied())
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct HotkeyScopes {
    stack: Vec<HotkeyScope>,
}

impl HotkeyScopes {
    #[must_use]
    pub fn from_outer_to_inner<I: IntoIterator<Item = HotkeyScope>>(scopes: I) -> Self {
        Self {
            stack: scopes.into_iter().collect(),
        }
    }

    pub fn push(&mut self, scope: HotkeyScope) {
        self.stack.push(scope);
    }

    pub fn pop(&mut self) -> Option<HotkeyScope> {
        self.stack.pop()
    }

    #[must_use]
    pub fn innermost_first(&self) -> impl DoubleEndedIterator<Item = &HotkeyScope> {
        self.stack.iter().rev()
    }
}

#[cfg(test)]
mod tests {
    use core::num::NonZeroU32;

    use super::{
        ActionId, HotkeyBinding, HotkeyScope, HotkeyScopes, HotkeyTable, HotkeyTableError, KeyChord,
    };
    use crate::input::{KeyChar, KeyCode, ModifierMask, NamedKey};

    fn scopes(s: &[HotkeyScope]) -> HotkeyScopes {
        HotkeyScopes::from_outer_to_inner(s.iter().copied())
    }

    fn action(n: u32) -> ActionId {
        let Some(nz) = NonZeroU32::new(n) else {
            panic!("test action id must be non-zero");
        };
        ActionId::new(nz)
    }

    fn ctrl_s() -> KeyChord {
        KeyChord::new(KeyCode::Char(KeyChar::from_char('s')), ModifierMask::CTRL)
    }

    fn esc() -> KeyChord {
        KeyChord::new(KeyCode::Named(NamedKey::Escape), ModifierMask::NONE)
    }

    #[test]
    fn duplicate_chord_in_same_scope_conflicts() {
        let result = HotkeyTable::try_from_bindings(vec![
            HotkeyBinding::new(ctrl_s(), HotkeyScope::Global, action(1)),
            HotkeyBinding::new(ctrl_s(), HotkeyScope::Global, action(2)),
        ]);
        match result {
            Err(HotkeyTableError::Conflict {
                existing,
                attempted,
                ..
            }) => {
                assert_eq!(existing, action(1));
                assert_eq!(attempted, action(2));
            }
            _ => panic!("expected conflict"),
        }
    }

    #[test]
    fn same_chord_different_scopes_does_not_conflict() {
        let table = HotkeyTable::try_from_bindings(vec![
            HotkeyBinding::new(esc(), HotkeyScope::Global, action(1)),
            HotkeyBinding::new(esc(), HotkeyScope::Modal, action(2)),
        ]);
        assert!(table.is_ok());
    }

    #[test]
    fn dispatch_innermost_scope_first() {
        let Ok(table) = HotkeyTable::try_from_bindings(vec![
            HotkeyBinding::new(esc(), HotkeyScope::Global, action(1)),
            HotkeyBinding::new(esc(), HotkeyScope::Modal, action(99)),
        ]) else {
            panic!("registration must succeed");
        };
        let with_modal = scopes(&[HotkeyScope::Global, HotkeyScope::Modal]);
        let no_modal = scopes(&[HotkeyScope::Global]);
        assert_eq!(table.dispatch(esc(), &with_modal), Some(action(99)));
        assert_eq!(table.dispatch(esc(), &no_modal), Some(action(1)));
    }

    #[test]
    fn dispatch_misses_unmatched_chord() {
        let Ok(table) = HotkeyTable::try_from_bindings(vec![HotkeyBinding::new(
            ctrl_s(),
            HotkeyScope::Global,
            action(1),
        )]) else {
            panic!("registration must succeed");
        };
        assert_eq!(table.dispatch(esc(), &scopes(&[HotkeyScope::Global])), None);
    }

    #[test]
    fn dispatch_ignores_inactive_scope() {
        let Ok(table) = HotkeyTable::try_from_bindings(vec![HotkeyBinding::new(
            esc(),
            HotkeyScope::Modal,
            action(7),
        )]) else {
            panic!("registration must succeed");
        };
        assert_eq!(table.dispatch(esc(), &scopes(&[HotkeyScope::Global])), None);
        assert_eq!(
            table.dispatch(esc(), &scopes(&[HotkeyScope::Modal])),
            Some(action(7))
        );
    }
}
