use bone_text::SourceByteIndex;
use unicode_segmentation::UnicodeSegmentation;

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

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum CaretMove {
    PrevGrapheme,
    NextGrapheme,
    PrevWord,
    NextWord,
    LineStart,
    LineEnd,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum SelectionAction {
    Collapse(CaretMove),
    Extend(CaretMove),
}

impl SelectionAction {
    #[must_use]
    pub fn from_key(event: KeyEvent) -> Option<Self> {
        let shift = event.modifiers.contains(ModifierMask::SHIFT);
        let ctrl = event.modifiers.contains(ModifierMask::CTRL);
        let blocked = event.modifiers.contains(ModifierMask::ALT)
            || event.modifiers.contains(ModifierMask::META);
        if blocked {
            return None;
        }
        let m = match (event.code, ctrl) {
            (KeyCode::Named(NamedKey::ArrowLeft), false) => CaretMove::PrevGrapheme,
            (KeyCode::Named(NamedKey::ArrowLeft), true) => CaretMove::PrevWord,
            (KeyCode::Named(NamedKey::ArrowRight), false) => CaretMove::NextGrapheme,
            (KeyCode::Named(NamedKey::ArrowRight), true) => CaretMove::NextWord,
            (KeyCode::Named(NamedKey::Home), false) => CaretMove::LineStart,
            (KeyCode::Named(NamedKey::End), false) => CaretMove::LineEnd,
            _ => return None,
        };
        Some(if shift {
            Self::Extend(m)
        } else {
            Self::Collapse(m)
        })
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Selection {
    anchor: SourceByteIndex,
    caret: SourceByteIndex,
}

impl Selection {
    #[must_use]
    pub const fn caret_at(caret: SourceByteIndex) -> Self {
        Self {
            anchor: caret,
            caret,
        }
    }

    #[must_use]
    pub const fn ranged(anchor: SourceByteIndex, caret: SourceByteIndex) -> Self {
        Self { anchor, caret }
    }

    #[must_use]
    pub const fn anchor(self) -> SourceByteIndex {
        self.anchor
    }

    #[must_use]
    pub const fn caret(self) -> SourceByteIndex {
        self.caret
    }

    #[must_use]
    pub fn is_empty(self) -> bool {
        self.anchor == self.caret
    }

    #[must_use]
    pub fn min(self) -> SourceByteIndex {
        SourceByteIndex::new(self.anchor.value().min(self.caret.value()))
    }

    #[must_use]
    pub fn max(self) -> SourceByteIndex {
        SourceByteIndex::new(self.anchor.value().max(self.caret.value()))
    }

    #[must_use]
    pub fn apply(self, text: &str, action: SelectionAction) -> Self {
        match action {
            SelectionAction::Collapse(m) => Self::caret_at(self.collapse_target(text, m)),
            SelectionAction::Extend(m) => Self {
                anchor: self.anchor,
                caret: step(text, self.caret, m),
            },
        }
    }

    fn collapse_target(self, text: &str, m: CaretMove) -> SourceByteIndex {
        if self.is_empty() {
            return step(text, self.caret, m);
        }
        match m {
            CaretMove::PrevGrapheme | CaretMove::PrevWord => self.min(),
            CaretMove::NextGrapheme | CaretMove::NextWord => self.max(),
            CaretMove::LineStart | CaretMove::LineEnd => step(text, self.caret, m),
        }
    }

    #[must_use]
    pub fn apply_key(self, text: &str, event: KeyEvent) -> Option<Self> {
        SelectionAction::from_key(event).map(|action| self.apply(text, action))
    }
}

fn step(text: &str, from: SourceByteIndex, m: CaretMove) -> SourceByteIndex {
    let next = match m {
        CaretMove::PrevGrapheme => prev_grapheme(text, from.value()),
        CaretMove::NextGrapheme => next_grapheme(text, from.value()),
        CaretMove::PrevWord => prev_word(text, from.value()),
        CaretMove::NextWord => next_word(text, from.value()),
        CaretMove::LineStart => 0,
        CaretMove::LineEnd => text.len(),
    };
    SourceByteIndex::new(next)
}

fn next_grapheme(text: &str, offset: usize) -> usize {
    text.grapheme_indices(true)
        .find(|&(i, _)| i > offset)
        .map_or(text.len(), |(i, _)| i)
}

fn prev_grapheme(text: &str, offset: usize) -> usize {
    text.grapheme_indices(true)
        .rev()
        .find(|&(i, _)| i < offset)
        .map_or(0, |(i, _)| i)
}

fn is_word_segment(segment: &str) -> bool {
    segment.chars().next().is_some_and(char::is_alphanumeric)
}

fn next_word(text: &str, offset: usize) -> usize {
    text.split_word_bound_indices()
        .find(|&(start, segment)| start > offset && is_word_segment(segment))
        .map_or(text.len(), |(start, _)| start)
}

fn prev_word(text: &str, offset: usize) -> usize {
    text.split_word_bound_indices()
        .rev()
        .find(|&(start, segment)| start < offset && is_word_segment(segment))
        .map_or(0, |(start, _)| start)
}

#[cfg(test)]
mod tests {
    use super::{CaretMove, Selection, SelectionAction};
    use crate::input::{KeyChar, KeyCode, KeyEvent, ModifierMask, NamedKey};
    use bone_text::SourceByteIndex;

    fn b(n: usize) -> SourceByteIndex {
        SourceByteIndex::new(n)
    }

    fn key(named: NamedKey, modifiers: ModifierMask) -> KeyEvent {
        KeyEvent::new(KeyCode::Named(named), modifiers)
    }

    #[test]
    fn caret_at_starts_empty_selection() {
        let s = Selection::caret_at(b(0));
        assert!(s.is_empty());
        assert_eq!(s.caret(), b(0));
        assert_eq!(s.anchor(), b(0));
    }

    #[test]
    fn ranged_min_max_independent_of_direction() {
        let forward = Selection::ranged(b(2), b(7));
        let backward = Selection::ranged(b(7), b(2));
        assert_eq!(forward.min(), b(2));
        assert_eq!(forward.max(), b(7));
        assert_eq!(backward.min(), b(2));
        assert_eq!(backward.max(), b(7));
        assert!(!forward.is_empty());
        assert!(!backward.is_empty());
    }

    #[test]
    fn collapse_next_grapheme_advances_one_codepoint_in_ascii() {
        let s = Selection::caret_at(b(0))
            .apply("abc", SelectionAction::Collapse(CaretMove::NextGrapheme));
        assert_eq!(s.caret(), b(1));
        assert!(s.is_empty());
    }

    #[test]
    fn collapse_prev_grapheme_retreats_one_codepoint_in_ascii() {
        let s = Selection::caret_at(b(2))
            .apply("abc", SelectionAction::Collapse(CaretMove::PrevGrapheme));
        assert_eq!(s.caret(), b(1));
    }

    #[test]
    fn collapse_clamps_at_text_bounds() {
        let end = Selection::caret_at(b(3))
            .apply("abc", SelectionAction::Collapse(CaretMove::NextGrapheme));
        assert_eq!(end.caret(), b(3));
        let start = Selection::caret_at(b(0))
            .apply("abc", SelectionAction::Collapse(CaretMove::PrevGrapheme));
        assert_eq!(start.caret(), b(0));
    }

    #[test]
    fn grapheme_step_keeps_combining_mark_with_base() {
        let text = "e\u{0301}f";
        let combined = Selection::caret_at(b(0))
            .apply(text, SelectionAction::Collapse(CaretMove::NextGrapheme));
        assert_eq!(combined.caret(), b(3), "right arrow must clear e + acute");
        let back = Selection::caret_at(b(3))
            .apply(text, SelectionAction::Collapse(CaretMove::PrevGrapheme));
        assert_eq!(back.caret(), b(0), "left arrow must restore base position");
    }

    #[test]
    fn grapheme_step_snaps_mid_cluster_offset_to_boundary() {
        let text = "e\u{0301}";
        let snap = Selection::caret_at(b(1))
            .apply(text, SelectionAction::Collapse(CaretMove::NextGrapheme));
        assert_eq!(snap.caret(), b(3));
        let back = Selection::caret_at(b(2))
            .apply(text, SelectionAction::Collapse(CaretMove::PrevGrapheme));
        assert_eq!(back.caret(), b(0));
    }

    #[test]
    fn collapse_arrow_on_non_empty_selection_collapses_to_edge() {
        let sel = Selection::ranged(b(2), b(5));
        let right = sel.apply(
            "abcdefg",
            SelectionAction::Collapse(CaretMove::NextGrapheme),
        );
        assert_eq!(right.caret(), b(5));
        assert!(right.is_empty());
        let left = sel.apply(
            "abcdefg",
            SelectionAction::Collapse(CaretMove::PrevGrapheme),
        );
        assert_eq!(left.caret(), b(2));
        assert!(left.is_empty());
    }

    #[test]
    fn collapse_word_on_non_empty_selection_collapses_to_edge() {
        let sel = Selection::ranged(b(2), b(5));
        let right = sel.apply(
            "abc def ghi",
            SelectionAction::Collapse(CaretMove::NextWord),
        );
        assert_eq!(right.caret(), b(5));
        assert!(right.is_empty());
        let left = sel.apply(
            "abc def ghi",
            SelectionAction::Collapse(CaretMove::PrevWord),
        );
        assert_eq!(left.caret(), b(2));
        assert!(left.is_empty());
    }

    #[test]
    fn collapse_line_bound_on_non_empty_selection_jumps_to_bound() {
        let sel = Selection::ranged(b(2), b(5));
        let home = sel.apply(
            "hello world",
            SelectionAction::Collapse(CaretMove::LineStart),
        );
        assert_eq!(home.caret(), b(0));
        assert!(home.is_empty());
        let end = sel.apply("hello world", SelectionAction::Collapse(CaretMove::LineEnd));
        assert_eq!(end.caret(), b(11));
        assert!(end.is_empty());
    }

    #[test]
    fn extend_keeps_anchor_and_moves_caret() {
        let s = Selection::caret_at(b(2))
            .apply("abcdef", SelectionAction::Extend(CaretMove::NextGrapheme));
        assert_eq!(s.anchor(), b(2));
        assert_eq!(s.caret(), b(3));
        assert!(!s.is_empty());
    }

    #[test]
    fn extend_supports_inverse_directions() {
        let forward = Selection::caret_at(b(3))
            .apply("abcdef", SelectionAction::Extend(CaretMove::NextGrapheme));
        let back = forward.apply("abcdef", SelectionAction::Extend(CaretMove::PrevGrapheme));
        assert_eq!(back.anchor(), b(3));
        assert_eq!(back.caret(), b(3));
        assert!(back.is_empty());
    }

    #[test]
    fn next_word_jumps_to_start_of_following_word() {
        let text = "abc def ghi";
        let from0 =
            Selection::caret_at(b(0)).apply(text, SelectionAction::Collapse(CaretMove::NextWord));
        assert_eq!(from0.caret(), b(4));
        let from4 =
            Selection::caret_at(b(4)).apply(text, SelectionAction::Collapse(CaretMove::NextWord));
        assert_eq!(from4.caret(), b(8));
        let from8 =
            Selection::caret_at(b(8)).apply(text, SelectionAction::Collapse(CaretMove::NextWord));
        assert_eq!(from8.caret(), b(11));
    }

    #[test]
    fn prev_word_jumps_to_start_of_current_or_previous_word() {
        let text = "abc def ghi";
        let from11 =
            Selection::caret_at(b(11)).apply(text, SelectionAction::Collapse(CaretMove::PrevWord));
        assert_eq!(from11.caret(), b(8));
        let from5 =
            Selection::caret_at(b(5)).apply(text, SelectionAction::Collapse(CaretMove::PrevWord));
        assert_eq!(from5.caret(), b(4));
        let from4 =
            Selection::caret_at(b(4)).apply(text, SelectionAction::Collapse(CaretMove::PrevWord));
        assert_eq!(from4.caret(), b(0));
    }

    #[test]
    fn word_jump_skips_runs_of_punctuation_and_whitespace() {
        let text = "abc !!! def";
        let next =
            Selection::caret_at(b(3)).apply(text, SelectionAction::Collapse(CaretMove::NextWord));
        assert_eq!(next.caret(), b(8));
        let prev =
            Selection::caret_at(b(8)).apply(text, SelectionAction::Collapse(CaretMove::PrevWord));
        assert_eq!(prev.caret(), b(0));
    }

    #[test]
    fn word_jump_treats_decomposed_combining_mark_as_part_of_word() {
        let text = "abc\u{0301}def ghi";
        let cluster_end = 8;
        let prev_from_cluster_end = Selection::caret_at(b(cluster_end))
            .apply(text, SelectionAction::Collapse(CaretMove::PrevWord));
        assert_eq!(
            prev_from_cluster_end.caret(),
            b(0),
            "combining mark inside cluster must not split the word",
        );
        let next_from_start =
            Selection::caret_at(b(0)).apply(text, SelectionAction::Collapse(CaretMove::NextWord));
        assert_eq!(next_from_start.caret(), b(cluster_end + 1));
    }

    #[test]
    fn line_start_and_end_clamp_to_text_bounds() {
        let text = "hello";
        let home =
            Selection::caret_at(b(3)).apply(text, SelectionAction::Collapse(CaretMove::LineStart));
        assert_eq!(home.caret(), b(0));
        let end =
            Selection::caret_at(b(0)).apply(text, SelectionAction::Collapse(CaretMove::LineEnd));
        assert_eq!(end.caret(), b(text.len()));
    }

    #[test]
    fn shift_arrow_dispatches_extend_action() {
        let action = SelectionAction::from_key(key(NamedKey::ArrowRight, ModifierMask::SHIFT));
        assert_eq!(
            action,
            Some(SelectionAction::Extend(CaretMove::NextGrapheme))
        );
    }

    #[test]
    fn ctrl_arrow_dispatches_word_jump() {
        let right = SelectionAction::from_key(key(NamedKey::ArrowRight, ModifierMask::CTRL));
        assert_eq!(right, Some(SelectionAction::Collapse(CaretMove::NextWord)));
        let left = SelectionAction::from_key(key(NamedKey::ArrowLeft, ModifierMask::CTRL));
        assert_eq!(left, Some(SelectionAction::Collapse(CaretMove::PrevWord)));
    }

    #[test]
    fn ctrl_shift_arrow_dispatches_extend_word() {
        let mods = ModifierMask::CTRL | ModifierMask::SHIFT;
        let action = SelectionAction::from_key(key(NamedKey::ArrowRight, mods));
        assert_eq!(action, Some(SelectionAction::Extend(CaretMove::NextWord)));
    }

    #[test]
    fn home_end_dispatch_to_line_bounds() {
        let home = SelectionAction::from_key(key(NamedKey::Home, ModifierMask::NONE));
        assert_eq!(home, Some(SelectionAction::Collapse(CaretMove::LineStart)));
        let shift_end = SelectionAction::from_key(key(NamedKey::End, ModifierMask::SHIFT));
        assert_eq!(shift_end, Some(SelectionAction::Extend(CaretMove::LineEnd)));
    }

    #[test]
    fn alt_or_meta_modifier_blocks_dispatch() {
        let alt = SelectionAction::from_key(key(NamedKey::ArrowRight, ModifierMask::ALT));
        assert!(alt.is_none());
        let meta = SelectionAction::from_key(key(NamedKey::ArrowRight, ModifierMask::META));
        assert!(meta.is_none());
    }

    #[test]
    fn unrelated_keys_yield_none() {
        let tab = SelectionAction::from_key(key(NamedKey::Tab, ModifierMask::NONE));
        assert!(tab.is_none());
        let printable = SelectionAction::from_key(KeyEvent::new(
            KeyCode::Char(KeyChar::from_char('x')),
            ModifierMask::NONE,
        ));
        assert!(printable.is_none());
    }

    #[test]
    fn apply_key_returns_none_for_non_selection_event() {
        let s = Selection::caret_at(b(2));
        let out = s.apply_key("hello", key(NamedKey::Tab, ModifierMask::NONE));
        assert!(out.is_none());
    }

    #[test]
    fn apply_key_round_trip_extends_with_shift_and_collapses_without() {
        let text = "hello";
        let extended = Selection::caret_at(b(0))
            .apply_key(text, key(NamedKey::ArrowRight, ModifierMask::SHIFT));
        let Some(extended) = extended else {
            panic!("shift+right must dispatch")
        };
        assert_eq!(extended.anchor(), b(0));
        assert_eq!(extended.caret(), b(1));
        let collapsed = extended.apply_key(text, key(NamedKey::ArrowRight, ModifierMask::NONE));
        let Some(collapsed) = collapsed else {
            panic!("right must dispatch")
        };
        assert_eq!(collapsed.caret(), b(1));
        assert!(collapsed.is_empty());
    }
}
