use antlr4_runtime::atn::IntervalSet;

use super::unicode::property_ranges;

const MAX_CODE_POINT: i32 = 0x10_ffff;

#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) enum EscapeSequenceResult {
    Invalid,
    CodePoint {
        value: i32,
        start: usize,
        stop: usize,
    },
    Property {
        code_points: IntervalSet,
        start: usize,
        stop: usize,
    },
}

pub(super) fn parse_escape(text: &str, start: usize) -> EscapeSequenceResult {
    let Some(tail) = text.get(start..) else {
        return EscapeSequenceResult::Invalid;
    };
    let mut characters = tail.char_indices();
    if characters.next().is_none_or(|(_, value)| value != '\\') {
        return EscapeSequenceResult::Invalid;
    }
    let Some((escaped_start, escaped)) = characters.next() else {
        return EscapeSequenceResult::Invalid;
    };
    let cursor = escaped_start + escaped.len_utf8();
    match escaped {
        'u' => parse_unicode_escape(tail, start, cursor),
        'p' | 'P' => parse_property_escape(tail, start, cursor, escaped == 'P'),
        _ => simple_escape(escaped).map_or(EscapeSequenceResult::Invalid, |value| {
            EscapeSequenceResult::CodePoint {
                value,
                start,
                stop: start + cursor,
            }
        }),
    }
}

fn parse_unicode_escape(tail: &str, start: usize, cursor: usize) -> EscapeSequenceResult {
    if cursor + 3 > tail.len() {
        return EscapeSequenceResult::Invalid;
    }
    let (digits, stop) = if tail.as_bytes().get(cursor) == Some(&b'{') {
        let digits_start = cursor + 1;
        let Some(close) = tail[digits_start..].find('}') else {
            return EscapeSequenceResult::Invalid;
        };
        let close = digits_start + close;
        (&tail[digits_start..close], close + 1)
    } else {
        let Some(digits) = tail.get(cursor..cursor + 4) else {
            return EscapeSequenceResult::Invalid;
        };
        (digits, cursor + 4)
    };
    let Ok(value) = i32::from_str_radix(digits, 16) else {
        return EscapeSequenceResult::Invalid;
    };
    if value > MAX_CODE_POINT {
        return EscapeSequenceResult::Invalid;
    }
    EscapeSequenceResult::CodePoint {
        value,
        start,
        stop: start + stop,
    }
}

fn parse_property_escape(
    tail: &str,
    start: usize,
    cursor: usize,
    inverted: bool,
) -> EscapeSequenceResult {
    if cursor + 3 > tail.len() || tail.as_bytes().get(cursor) != Some(&b'{') {
        return EscapeSequenceResult::Invalid;
    }
    let name_start = cursor + 1;
    let Some(close) = tail[name_start..].find('}') else {
        return EscapeSequenceResult::Invalid;
    };
    let close = name_start + close;
    let Some(ranges) =
        property_ranges(&tail[name_start..close]).filter(|ranges| !ranges.is_empty())
    else {
        return EscapeSequenceResult::Invalid;
    };
    let code_points = if inverted {
        complement(ranges)
    } else {
        interval_set(ranges)
    };
    EscapeSequenceResult::Property {
        code_points,
        start,
        stop: start + close + 1,
    }
}

fn simple_escape(escaped: char) -> Option<i32> {
    match escaped {
        'n' => Some(i32::from(b'\n')),
        'r' => Some(i32::from(b'\r')),
        't' => Some(i32::from(b'\t')),
        'b' => Some(i32::from(b'\x08')),
        'f' => Some(i32::from(b'\x0c')),
        '\\' => Some(i32::from(b'\\')),
        ']' => Some(i32::from(b']')),
        '-' => Some(i32::from(b'-')),
        _ => None,
    }
}

fn interval_set(ranges: &[i32]) -> IntervalSet {
    let mut result = IntervalSet::new();
    for range in ranges.chunks_exact(2) {
        result.add_range(range[0], range[1]);
    }
    result
}

fn complement(ranges: &[i32]) -> IntervalSet {
    let mut result = IntervalSet::new();
    let mut next = 0;
    for range in ranges.chunks_exact(2) {
        if next < range[0] {
            result.add_range(next, range[0] - 1);
        }
        next = range[1] + 1;
    }
    if next <= MAX_CODE_POINT {
        result.add_range(next, MAX_CODE_POINT);
    }
    result
}

#[cfg(test)]
#[allow(clippy::disallowed_methods)] // `insta` assertion macros unwrap internal I/O.
mod tests {
    use super::*;

    fn code_point(value: i32, stop: usize) -> EscapeSequenceResult {
        EscapeSequenceResult::CodePoint {
            value,
            start: 0,
            stop,
        }
    }

    fn property(code_points: IntervalSet, stop: usize) -> EscapeSequenceResult {
        EscapeSequenceResult::Property {
            code_points,
            start: 0,
            stop,
        }
    }

    #[test]
    fn parse_empty_matches_java() {
        assert_eq!(parse_escape("", 0), EscapeSequenceResult::Invalid);
    }

    #[test]
    fn parse_just_backslash_matches_java() {
        assert_eq!(parse_escape("\\", 0), EscapeSequenceResult::Invalid);
    }

    #[test]
    fn parse_invalid_escape_matches_java() {
        assert_eq!(parse_escape("\\z", 0), EscapeSequenceResult::Invalid);
    }

    #[test]
    fn parse_newline_matches_java() {
        assert_eq!(parse_escape("\\n", 0), code_point(i32::from(b'\n'), 2));
    }

    #[test]
    fn parse_tab_matches_java() {
        assert_eq!(parse_escape("\\t", 0), code_point(i32::from(b'\t'), 2));
    }

    #[test]
    fn parse_unicode_too_short_matches_java() {
        assert_eq!(parse_escape("\\uABC", 0), EscapeSequenceResult::Invalid);
    }

    #[test]
    fn parse_unicode_bmp_matches_java() {
        assert_eq!(parse_escape("\\uABCD", 0), code_point(0xabcd, 6));
    }

    #[test]
    fn parse_unicode_smp_too_short_matches_java() {
        assert_eq!(parse_escape("\\u{}", 0), EscapeSequenceResult::Invalid);
    }

    #[test]
    fn parse_unicode_smp_missing_close_brace_matches_java() {
        assert_eq!(parse_escape("\\u{12345", 0), EscapeSequenceResult::Invalid);
    }

    #[test]
    fn parse_unicode_too_big_matches_java() {
        assert_eq!(
            parse_escape("\\u{110000}", 0),
            EscapeSequenceResult::Invalid
        );
    }

    #[test]
    fn parse_unicode_smp_matches_java() {
        assert_eq!(parse_escape("\\u{10ABCD}", 0), code_point(0x10_abcd, 10));
    }

    #[test]
    fn parse_unicode_property_too_short_matches_java() {
        assert_eq!(parse_escape("\\p{}", 0), EscapeSequenceResult::Invalid);
    }

    #[test]
    fn parse_unicode_property_missing_close_brace_matches_java() {
        assert_eq!(parse_escape("\\p{1234", 0), EscapeSequenceResult::Invalid);
    }

    #[test]
    fn parse_unicode_property_matches_java() {
        assert_eq!(
            parse_escape("\\p{Deseret}", 0),
            property(IntervalSet::from_range(66_560, 66_639), 11)
        );
    }

    #[test]
    fn parse_unicode_property_inverted_too_short_matches_java() {
        assert_eq!(parse_escape("\\P{}", 0), EscapeSequenceResult::Invalid);
    }

    #[test]
    fn parse_unicode_property_inverted_missing_close_brace_matches_java() {
        assert_eq!(
            parse_escape("\\P{Deseret", 0),
            EscapeSequenceResult::Invalid
        );
    }

    #[test]
    fn parse_unicode_property_inverted_matches_java() {
        // The `\P` complement spans two coalesced ranges (0..=66_559, 66_640..=0x10_ffff); the
        // whole EscapeSequenceResult::Property is clearer as one inline snapshot than as a
        // hand-assembled `expected` set.
        insta::assert_debug_snapshot!(parse_escape("\\P{Deseret}", 0), @"
        Property {
            code_points: IntervalSet {
                ranges: [
                    (
                        0,
                        66559,
                    ),
                    (
                        66640,
                        1114111,
                    ),
                ],
            },
            start: 0,
            stop: 11,
        }
        ");
    }
}
