use ropey::RopeSlice;

use crate::chars::{categorize_char, char_is_line_ending, char_is_whitespace, CharCategory};
use crate::movement::{self, Direction};
use crate::surround;
use crate::Range;

fn this_word_end_pos(slice: RopeSlice, pos: usize) -> usize {
    this_word_bound_pos(slice, pos, Direction::Forward)
}

fn this_word_start_pos(slice: RopeSlice, pos: usize) -> usize {
    this_word_bound_pos(slice, pos, Direction::Backward)
}

fn this_word_bound_pos(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize {
    let iter = match direction {
        Direction::Forward => slice.chars_at(pos + 1),
        Direction::Backward => {
            let mut iter = slice.chars_at(pos);
            iter.reverse();
            iter
        }
    };

    match categorize_char(slice.char(pos)) {
        CharCategory::Eol | CharCategory::Whitespace => pos,
        category => {
            for peek in iter {
                let curr_category = categorize_char(peek);
                if curr_category != category
                    || curr_category == CharCategory::Eol
                    || curr_category == CharCategory::Whitespace
                {
                    return pos;
                }
                pos = match direction {
                    Direction::Forward => pos + 1,
                    Direction::Backward => pos.saturating_sub(1),
                }
            }
            pos
        }
    }
}

#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum TextObject {
    Around,
    Inside,
}

// count doesn't do anything yet
pub fn textobject_word(
    slice: RopeSlice,
    range: Range,
    textobject: TextObject,
    count: usize,
) -> Range {
    let this_word_start = this_word_start_pos(slice, range.head);
    let this_word_end = this_word_end_pos(slice, range.head);

    let (anchor, head);
    match textobject {
        TextObject::Inside => {
            anchor = this_word_start;
            head = this_word_end;
        }
        TextObject::Around => {
            if slice
                .get_char(this_word_end + 1)
                .map_or(true, char_is_line_ending)
            {
                head = this_word_end;
                if slice
                    .get_char(this_word_start.saturating_sub(1))
                    .map_or(true, char_is_line_ending)
                {
                    // single word on a line
                    anchor = this_word_start;
                } else {
                    // last word on a line, select the whitespace before it too
                    anchor = movement::move_prev_word_end(slice, range, count).head;
                }
            } else if char_is_whitespace(slice.char(range.head)) {
                // select whole whitespace and next word
                head = movement::move_next_word_end(slice, range, count).head;
                anchor = movement::backwards_skip_while(slice, range.head, |c| c.is_whitespace())
                    .map(|p| p + 1) // p is first *non* whitespace char, so +1 to get whitespace pos
                    .unwrap_or(0);
            } else {
                head = movement::move_next_word_start(slice, range, count).head;
                anchor = this_word_start;
            }
        }
    };
    Range::new(anchor, head)
}

pub fn textobject_surround(
    slice: RopeSlice,
    range: Range,
    textobject: TextObject,
    ch: char,
    count: usize,
) -> Range {
    surround::find_nth_pairs_pos(slice, ch, range.head, count)
        .map(|(anchor, head)| match textobject {
            TextObject::Inside => Range::new(anchor + 1, head.saturating_sub(1)),
            TextObject::Around => Range::new(anchor, head),
        })
        .unwrap_or(range)
}

#[cfg(test)]
mod test {
    use super::TextObject::*;
    use super::*;

    use crate::Range;
    use ropey::Rope;

    #[test]
    fn test_textobject_word() {
        // (text, [(cursor position, textobject, final range), ...])
        let tests = &[
            (
                "cursor at beginning of doc",
                vec![(0, Inside, (0, 5)), (0, Around, (0, 6))],
            ),
            (
                "cursor at middle of word",
                vec![
                    (13, Inside, (10, 15)),
                    (10, Inside, (10, 15)),
                    (15, Inside, (10, 15)),
                    (13, Around, (10, 16)),
                    (10, Around, (10, 16)),
                    (15, Around, (10, 16)),
                ],
            ),
            (
                "cursor between word whitespace",
                vec![(6, Inside, (6, 6)), (6, Around, (6, 13))],
            ),
            (
                "cursor on word before newline\n",
                vec![
                    (22, Inside, (22, 28)),
                    (28, Inside, (22, 28)),
                    (25, Inside, (22, 28)),
                    (22, Around, (21, 28)),
                    (28, Around, (21, 28)),
                    (25, Around, (21, 28)),
                ],
            ),
            (
                "cursor on newline\nnext line",
                vec![(17, Inside, (17, 17)), (17, Around, (17, 22))],
            ),
            (
                "cursor on word after newline\nnext line",
                vec![
                    (29, Inside, (29, 32)),
                    (30, Inside, (29, 32)),
                    (32, Inside, (29, 32)),
                    (29, Around, (29, 33)),
                    (30, Around, (29, 33)),
                    (32, Around, (29, 33)),
                ],
            ),
            (
                "cursor on #$%:;* punctuation",
                vec![
                    (13, Inside, (10, 15)),
                    (10, Inside, (10, 15)),
                    (15, Inside, (10, 15)),
                    (13, Around, (10, 16)),
                    (10, Around, (10, 16)),
                    (15, Around, (10, 16)),
                ],
            ),
            (
                "cursor on punc%^#$:;.tuation",
                vec![
                    (14, Inside, (14, 20)),
                    (20, Inside, (14, 20)),
                    (17, Inside, (14, 20)),
                    (14, Around, (14, 20)),
                    // FIXME: edge case
                    // (20, Around, (14, 20)),
                    (17, Around, (14, 20)),
                ],
            ),
            (
                "cursor in   extra whitespace",
                vec![
                    (9, Inside, (9, 9)),
                    (10, Inside, (10, 10)),
                    (11, Inside, (11, 11)),
                    (9, Around, (9, 16)),
                    (10, Around, (9, 16)),
                    (11, Around, (9, 16)),
                ],
            ),
            (
                "cursor at end of doc",
                vec![(19, Inside, (17, 19)), (19, Around, (16, 19))],
            ),
        ];

        for (sample, scenario) in tests {
            let doc = Rope::from(*sample);
            let slice = doc.slice(..);
            for &case in scenario {
                let (pos, objtype, expected_range) = case;
                let result = textobject_word(slice, Range::point(pos), objtype, 1);
                assert_eq!(
                    result,
                    expected_range.into(),
                    "\nCase failed: {:?} - {:?}",
                    sample,
                    case
                );
            }
        }
    }

    #[test]
    fn test_textobject_surround() {
        // (text, [(cursor position, textobject, final range, count), ...])
        let tests = &[
            (
                "simple (single) surround pairs",
                vec![
                    (3, Inside, (3, 3), '(', 1),
                    (7, Inside, (8, 13), ')', 1),
                    (10, Inside, (8, 13), '(', 1),
                    (14, Inside, (8, 13), ')', 1),
                    (3, Around, (3, 3), '(', 1),
                    (7, Around, (7, 14), ')', 1),
                    (10, Around, (7, 14), '(', 1),
                    (14, Around, (7, 14), ')', 1),
                ],
            ),
            (
                "samexx 'single' surround pairs",
                vec![
                    (3, Inside, (3, 3), '\'', 1),
                    (7, Inside, (8, 13), '\'', 1),
                    (10, Inside, (8, 13), '\'', 1),
                    (14, Inside, (8, 13), '\'', 1),
                    (3, Around, (3, 3), '\'', 1),
                    (7, Around, (7, 14), '\'', 1),
                    (10, Around, (7, 14), '\'', 1),
                    (14, Around, (7, 14), '\'', 1),
                ],
            ),
            (
                "(nested (surround (pairs)) 3 levels)",
                vec![
                    (0, Inside, (1, 34), '(', 1),
                    (6, Inside, (1, 34), ')', 1),
                    (8, Inside, (9, 24), '(', 1),
                    (8, Inside, (9, 34), ')', 2),
                    (20, Inside, (9, 24), '(', 2),
                    (20, Inside, (1, 34), ')', 3),
                    (0, Around, (0, 35), '(', 1),
                    (6, Around, (0, 35), ')', 1),
                    (8, Around, (8, 25), '(', 1),
                    (8, Around, (8, 35), ')', 2),
                    (20, Around, (8, 25), '(', 2),
                    (20, Around, (0, 35), ')', 3),
                ],
            ),
            (
                "(mixed {surround [pair] same} line)",
                vec![
                    (2, Inside, (1, 33), '(', 1),
                    (9, Inside, (8, 27), '{', 1),
                    (18, Inside, (18, 21), '[', 1),
                    (2, Around, (0, 34), '(', 1),
                    (9, Around, (7, 28), '{', 1),
                    (18, Around, (17, 22), '[', 1),
                ],
            ),
            (
                "(stepped (surround) pairs (should) skip)",
                vec![(22, Inside, (1, 38), '(', 1), (22, Around, (0, 39), '(', 1)],
            ),
            (
                "[surround pairs{\non different]\nlines}",
                vec![
                    (7, Inside, (1, 28), '[', 1),
                    (15, Inside, (16, 35), '{', 1),
                    (7, Around, (0, 29), '[', 1),
                    (15, Around, (15, 36), '{', 1),
                ],
            ),
        ];

        for (sample, scenario) in tests {
            let doc = Rope::from(*sample);
            let slice = doc.slice(..);
            for &case in scenario {
                let (pos, objtype, expected_range, ch, count) = case;
                let result = textobject_surround(slice, Range::point(pos), objtype, ch, count);
                assert_eq!(
                    result,
                    expected_range.into(),
                    "\nCase failed: {:?} - {:?}",
                    sample,
                    case
                );
            }
        }
    }
}
