use crate::{
    diag::Diagnostic,
    token::{Span, Token, TokenKind},
};

pub fn lex(file: &str, source: &str) -> Result<Vec<Token>, Vec<Diagnostic>> {
    let bytes = source.as_bytes();
    let mut tokens = Vec::new();
    let mut errors = Vec::new();
    let mut i = 0;

    while i < bytes.len() {
        let c = bytes[i] as char;

        match c {
            '(' => {
                tokens.push(Token {
                    kind: TokenKind::LParen,
                    span: Span::new(i, i + 1),
                });
                i += 1;
            }
            ')' => {
                tokens.push(Token {
                    kind: TokenKind::RParen,
                    span: Span::new(i, i + 1),
                });
                i += 1;
            }
            ';' => {
                while i < bytes.len() && bytes[i] as char != '\n' {
                    i += 1;
                }
            }
            '"' => {
                let start = i;
                i += 1;
                let mut value = String::new();

                while i < bytes.len() {
                    let ch = bytes[i] as char;
                    if ch == '"' {
                        i += 1;
                        tokens.push(Token {
                            kind: TokenKind::String(value),
                            span: Span::new(start, i),
                        });
                        break;
                    }

                    if ch == '\\' {
                        let escape_start = i;
                        i += 1;
                        if i >= bytes.len() {
                            errors.push(
                                Diagnostic::new(file, "UnterminatedString", "unterminated string")
                                    .with_span(Span::new(start, bytes.len())),
                            );
                            break;
                        }

                        let escaped = bytes[i] as char;
                        match escaped {
                            'n' => value.push('\n'),
                            't' => value.push('\t'),
                            '"' => value.push('"'),
                            '\\' => value.push('\\'),
                            other => errors.push(
                                Diagnostic::new(
                                    file,
                                    "UnsupportedStringEscape",
                                    "string literal uses an unsupported escape",
                                )
                                .with_span(Span::new(escape_start, i + 1))
                                .expected("\\n, \\t, \\\", or \\\\")
                                .found(format!("\\{}", other))
                                .hint(
                                    "the first string runtime slice supports only newline, tab, quote, and backslash escapes",
                                ),
                            ),
                        }
                        i += 1;
                    } else {
                        let literal_ch = source[i..].chars().next().expect("valid source slice");
                        let next_i = i + literal_ch.len_utf8();

                        if !literal_ch.is_ascii() || literal_ch.is_control() {
                            errors.push(
                                Diagnostic::new(
                                    file,
                                    "UnsupportedStringLiteral",
                                    "string literal contains bytes outside the first runtime string slice",
                                )
                                .with_span(Span::new(i, i + 1))
                                .expected("printable ASCII string literal")
                                .found("unsupported string literal")
                                .hint(
                                    "use printable ASCII text plus the current \\n, \\t, \\\", and \\\\ escapes",
                                ),
                            );
                            i = next_i;
                            continue;
                        }
                        value.push(literal_ch);
                        i = next_i;
                    }
                }

                if i >= bytes.len()
                    && !matches!(tokens.last().map(|t| &t.kind), Some(TokenKind::String(_)))
                {
                    errors.push(
                        Diagnostic::new(file, "UnterminatedString", "unterminated string")
                            .with_span(Span::new(start, bytes.len())),
                    );
                }
            }
            '-' if i + 1 < bytes.len() && bytes[i + 1] as char == '>' => {
                tokens.push(Token {
                    kind: TokenKind::Arrow,
                    span: Span::new(i, i + 2),
                });
                i += 2;
            }
            c if c.is_whitespace() => {
                i += 1;
            }
            _ => {
                let start = i;
                while i < bytes.len() {
                    let ch = bytes[i] as char;
                    if ch.is_whitespace() || ch == '(' || ch == ')' || ch == ';' {
                        break;
                    }
                    i += 1;
                }

                let text = &source[start..i];
                let span = Span::new(start, i);
                let kind = if let Some(result) = parse_i64_suffix_atom(text) {
                    match result {
                        Ok(value) => TokenKind::I64(value),
                        Err(error) => {
                            errors.push(error.to_diagnostic(file, text, span));
                            continue;
                        }
                    }
                } else if let Some(result) = parse_u32_suffix_atom(text) {
                    match result {
                        Ok(value) => TokenKind::U32(value),
                        Err(error) => {
                            errors.push(error.to_diagnostic(file, text, span));
                            continue;
                        }
                    }
                } else if let Some(result) = parse_u64_suffix_atom(text) {
                    match result {
                        Ok(value) => TokenKind::U64(value),
                        Err(error) => {
                            errors.push(error.to_diagnostic(file, text, span));
                            continue;
                        }
                    }
                } else if let Ok(value) = text.parse::<i64>() {
                    TokenKind::Int(value)
                } else if let Ok(value) = text.parse::<f64>() {
                    TokenKind::Float(value)
                } else {
                    TokenKind::Ident(text.to_string())
                };

                tokens.push(Token { kind, span });
            }
        }
    }

    if errors.is_empty() {
        Ok(tokens)
    } else {
        Err(errors)
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum I64SuffixError {
    InvalidDigits,
    OutOfRange,
}

impl I64SuffixError {
    fn to_diagnostic(self, file: &str, text: &str, span: Span) -> Diagnostic {
        match self {
            Self::InvalidDigits => Diagnostic::new(
                file,
                "InvalidI64Literal",
                "i64 literal must use signed decimal digits before the `i64` suffix",
            )
            .with_span(span)
            .expected("signed decimal digits followed by `i64`")
            .found(text)
            .hint("use forms like `42i64` or `-7i64`; other i64 literal forms remain deferred"),
            Self::OutOfRange => Diagnostic::new(
                file,
                "I64LiteralOutOfRange",
                "i64 literal is outside the supported signed 64-bit range",
            )
            .with_span(span)
            .expected("signed 64-bit integer literal with `i64` suffix")
            .found(text)
            .hint("use a value between -9223372036854775808i64 and 9223372036854775807i64"),
        }
    }
}

fn parse_i64_suffix_atom(text: &str) -> Option<Result<i64, I64SuffixError>> {
    let digits = text.strip_suffix("i64")?;
    if digits.is_empty() {
        return None;
    }
    if digits == "-" || digits == "+" || digits.starts_with('+') {
        return Some(Err(I64SuffixError::InvalidDigits));
    }

    let rest = digits.strip_prefix('-').unwrap_or(digits);
    if rest.is_empty() {
        return Some(Err(I64SuffixError::InvalidDigits));
    }

    if !rest.as_bytes()[0].is_ascii_digit() {
        if digits.starts_with('-') {
            return Some(Err(I64SuffixError::InvalidDigits));
        }
        return None;
    }

    if !rest.bytes().all(|byte| byte.is_ascii_digit()) {
        return Some(Err(I64SuffixError::InvalidDigits));
    }

    Some(
        digits
            .parse::<i64>()
            .map_err(|_| I64SuffixError::OutOfRange),
    )
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum U32SuffixError {
    InvalidDigits,
    OutOfRange,
}

impl U32SuffixError {
    fn to_diagnostic(self, file: &str, text: &str, span: Span) -> Diagnostic {
        match self {
            Self::InvalidDigits => Diagnostic::new(
                file,
                "InvalidU32Literal",
                "u32 literal must use unsigned decimal digits before the `u32` suffix",
            )
            .with_span(span)
            .expected("unsigned decimal digits followed by `u32`")
            .found(text)
            .hint("use forms like `42u32`; signed or non-decimal u32 literals remain deferred"),
            Self::OutOfRange => Diagnostic::new(
                file,
                "U32LiteralOutOfRange",
                "u32 literal is outside the supported unsigned 32-bit range",
            )
            .with_span(span)
            .expected("unsigned 32-bit integer literal with `u32` suffix")
            .found(text)
            .hint("use a value between 0u32 and 4294967295u32"),
        }
    }
}

fn parse_u32_suffix_atom(text: &str) -> Option<Result<u32, U32SuffixError>> {
    let digits = text.strip_suffix("u32")?;
    if digits.is_empty() {
        return None;
    }
    if digits.starts_with('-') || digits.starts_with('+') {
        return Some(Err(U32SuffixError::InvalidDigits));
    }
    if !digits.as_bytes()[0].is_ascii_digit() {
        return None;
    }
    if !digits.bytes().all(|byte| byte.is_ascii_digit()) {
        return Some(Err(U32SuffixError::InvalidDigits));
    }
    Some(
        digits
            .parse::<u32>()
            .map_err(|_| U32SuffixError::OutOfRange),
    )
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum U64SuffixError {
    InvalidDigits,
    OutOfRange,
}

impl U64SuffixError {
    fn to_diagnostic(self, file: &str, text: &str, span: Span) -> Diagnostic {
        match self {
            Self::InvalidDigits => Diagnostic::new(
                file,
                "InvalidU64Literal",
                "u64 literal must use unsigned decimal digits before the `u64` suffix",
            )
            .with_span(span)
            .expected("unsigned decimal digits followed by `u64`")
            .found(text)
            .hint("use forms like `42u64`; signed or non-decimal u64 literals remain deferred"),
            Self::OutOfRange => Diagnostic::new(
                file,
                "U64LiteralOutOfRange",
                "u64 literal is outside the supported unsigned 64-bit range",
            )
            .with_span(span)
            .expected("unsigned 64-bit integer literal with `u64` suffix")
            .found(text)
            .hint("use a value between 0u64 and 18446744073709551615u64"),
        }
    }
}

fn parse_u64_suffix_atom(text: &str) -> Option<Result<u64, U64SuffixError>> {
    let digits = text.strip_suffix("u64")?;
    if digits.is_empty() {
        return None;
    }
    if digits.starts_with('-') || digits.starts_with('+') {
        return Some(Err(U64SuffixError::InvalidDigits));
    }
    if !digits.as_bytes()[0].is_ascii_digit() {
        return None;
    }
    if !digits.bytes().all(|byte| byte.is_ascii_digit()) {
        return Some(Err(U64SuffixError::InvalidDigits));
    }
    Some(
        digits
            .parse::<u64>()
            .map_err(|_| U64SuffixError::OutOfRange),
    )
}
