//! Numeric literal lexing.

use crate::error::Error;
use crate::token::TokenKind;
use ecma_syntax_cat::span::Position;

/// Scan a numeric literal starting at byte `start`.  Returns the produced
/// token kind and the position just past the literal.
///
/// # Errors
///
/// Returns [`Error::InvalidNumber`] when the literal is malformed.
pub fn scan(src: &[u8], start: usize, at: Position) -> Result<(TokenKind, usize), Error> {
    src.get(start).copied().map_or(
        Err(Error::InvalidNumber {
            at,
            reason: "expected a digit",
        }),
        |b| dispatch(src, start, b, at),
    )
}

fn dispatch(
    src: &[u8],
    start: usize,
    first: u8,
    at: Position,
) -> Result<(TokenKind, usize), Error> {
    match first {
        b'0' => scan_zero_prefixed(src, start, at),
        b'1'..=b'9' => scan_decimal_from(src, start, at),
        b'.' => scan_decimal_fraction(src, start, at),
        _ => Err(Error::InvalidNumber {
            at,
            reason: "expected a digit or `.`",
        }),
    }
}

fn scan_zero_prefixed(src: &[u8], start: usize, at: Position) -> Result<(TokenKind, usize), Error> {
    match src.get(start + 1).copied() {
        Some(b'x' | b'X') => {
            scan_radix(src, start + 2, 16, "hex", is_hex_digit, hex_digit_value, at)
        }
        Some(b'o' | b'O') => scan_radix(
            src,
            start + 2,
            8,
            "octal",
            is_octal_digit,
            octal_digit_value,
            at,
        ),
        Some(b'b' | b'B') => scan_radix(
            src,
            start + 2,
            2,
            "binary",
            is_binary_digit,
            binary_digit_value,
            at,
        ),
        Some(b'n') => Ok((TokenKind::BigInt("0".to_owned()), start + 2)),
        Some(b'0'..=b'9') => Err(Error::InvalidNumber {
            at,
            reason: "legacy octal literals are not supported; use 0o prefix",
        }),
        _ => scan_decimal_from(src, start, at),
    }
}

fn scan_radix(
    src: &[u8],
    digits_start: usize,
    base: u32,
    label: &'static str,
    is_digit: fn(u8) -> bool,
    digit_value: fn(u8) -> u32,
    at: Position,
) -> Result<(TokenKind, usize), Error> {
    let end = scan_while(src, digits_start, is_digit);
    if end == digits_start {
        Err(Error::InvalidNumber {
            at,
            reason: match label {
                "hex" => "hex literal needs at least one hex digit",
                "octal" => "octal literal needs at least one octal digit",
                "binary" => "binary literal needs at least one binary digit",
                _other => "radix literal needs at least one digit",
            },
        })
    } else {
        let slice = src.get(digits_start..end).unwrap_or(&[]);
        if src.get(end).copied() == Some(b'n') {
            let decimal = convert_to_decimal(slice, base);
            Ok((TokenKind::BigInt(decimal), end + 1))
        } else {
            let value = slice.iter().fold(0.0_f64, |acc, &b| {
                acc * f64::from(base) + f64::from(digit_value(b))
            });
            Ok((TokenKind::Number(value), end))
        }
    }
}

fn scan_decimal_from(src: &[u8], start: usize, at: Position) -> Result<(TokenKind, usize), Error> {
    let int_end = scan_while(src, start, |b| b.is_ascii_digit());
    let fraction_start = int_end;
    let fraction_end = match src.get(int_end).copied() {
        Some(b'.') => scan_while(src, int_end + 1, |b| b.is_ascii_digit()),
        _other => fraction_start,
    };
    let after_fraction = if fraction_end > fraction_start {
        fraction_end
    } else {
        int_end
    };
    let exp_end = match src.get(after_fraction).copied() {
        Some(b'e' | b'E') => scan_exponent(src, after_fraction + 1, at)?,
        _other => after_fraction,
    };
    let has_fraction_or_exp = fraction_end > fraction_start || exp_end > after_fraction;
    if !has_fraction_or_exp && src.get(exp_end).copied() == Some(b'n') {
        let digits = std::str::from_utf8(src.get(start..exp_end).unwrap_or(&[]))
            .unwrap_or("")
            .to_owned();
        Ok((TokenKind::BigInt(digits), exp_end + 1))
    } else {
        let text = std::str::from_utf8(src.get(start..exp_end).unwrap_or(&[])).unwrap_or("");
        text.parse::<f64>()
            .map_err(|_| Error::InvalidNumber {
                at,
                reason: "could not parse decimal literal",
            })
            .map(|value| (TokenKind::Number(value), exp_end))
    }
}

fn scan_decimal_fraction(
    src: &[u8],
    start: usize,
    at: Position,
) -> Result<(TokenKind, usize), Error> {
    // Called with src[start] == b'.'; require at least one fraction digit.
    let frac_start = start + 1;
    let frac_end = scan_while(src, frac_start, |b| b.is_ascii_digit());
    if frac_end == frac_start {
        Err(Error::InvalidNumber {
            at,
            reason: "decimal literal beginning with `.` needs a fraction digit",
        })
    } else {
        let exp_end = match src.get(frac_end).copied() {
            Some(b'e' | b'E') => scan_exponent(src, frac_end + 1, at)?,
            _other => frac_end,
        };
        let text = std::str::from_utf8(src.get(start..exp_end).unwrap_or(&[])).unwrap_or("");
        text.parse::<f64>()
            .map_err(|_| Error::InvalidNumber {
                at,
                reason: "could not parse decimal literal",
            })
            .map(|value| (TokenKind::Number(value), exp_end))
    }
}

fn scan_exponent(src: &[u8], from: usize, at: Position) -> Result<usize, Error> {
    let after_sign = match src.get(from).copied() {
        Some(b'+' | b'-') => from + 1,
        _other => from,
    };
    let digits_end = scan_while(src, after_sign, |b| b.is_ascii_digit());
    if digits_end == after_sign {
        Err(Error::InvalidNumber {
            at,
            reason: "exponent needs at least one digit",
        })
    } else {
        Ok(digits_end)
    }
}

fn scan_while<F: Fn(u8) -> bool>(src: &[u8], from: usize, pred: F) -> usize {
    src.get(from)
        .copied()
        .filter(|b| pred(*b))
        .map_or(from, |_| scan_while(src, from + 1, pred))
}

fn convert_to_decimal(digits: &[u8], base: u32) -> String {
    // Build a decimal string by folding through a u128 accumulator.  For
    // BigInt sources larger than 2^128 we silently truncate; full arbitrary-
    // precision conversion is deferred to the runtime layer.
    let value: u128 = digits.iter().fold(0_u128, |acc, &b| {
        acc * u128::from(base) + u128::from(hex_digit_value(b))
    });
    format!("{value}")
}

fn is_hex_digit(b: u8) -> bool {
    b.is_ascii_hexdigit()
}

fn is_octal_digit(b: u8) -> bool {
    matches!(b, b'0'..=b'7')
}

fn is_binary_digit(b: u8) -> bool {
    matches!(b, b'0' | b'1')
}

fn hex_digit_value(b: u8) -> u32 {
    match b {
        b'0'..=b'9' => u32::from(b - b'0'),
        b'a'..=b'f' => u32::from(b - b'a' + 10),
        b'A'..=b'F' => u32::from(b - b'A' + 10),
        _other => 0,
    }
}

fn octal_digit_value(b: u8) -> u32 {
    match b {
        b'0'..=b'7' => u32::from(b - b'0'),
        _other => 0,
    }
}

fn binary_digit_value(b: u8) -> u32 {
    match b {
        b'0' | b'1' => u32::from(b - b'0'),
        _other => 0,
    }
}
