//! String literal lexing with escape decoding.

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

/// One peek result.
enum Step {
    End,
    Byte(u8),
}

fn peek(src: &[u8], pos: usize) -> Step {
    src.get(pos).copied().map_or(Step::End, Step::Byte)
}

/// Scan a string literal starting at byte `start` (which points to the
/// opening quote).  Returns the decoded content and the byte position just
/// past the closing quote.
///
/// # Errors
///
/// Returns [`Error::UnterminatedString`] on EOF or raw line terminator,
/// or [`Error::InvalidEscape`] / [`Error::InvalidUnicodeEscape`] for
/// malformed escape sequences.
pub fn scan(src: &[u8], start: usize, at: Position) -> Result<(String, usize), Error> {
    let quote = src
        .get(start)
        .copied()
        .ok_or(Error::UnterminatedString { at })?;
    scan_chars(src, start + 1, String::new(), quote, at)
}

fn scan_chars(
    src: &[u8],
    pos: usize,
    acc: String,
    quote: u8,
    at: Position,
) -> Result<(String, usize), Error> {
    match peek(src, pos) {
        Step::End | Step::Byte(b'\n' | b'\r') => Err(Error::UnterminatedString { at }),
        Step::Byte(b) if b == quote => Ok((acc, pos + 1)),
        Step::Byte(b'\\') => handle_escape(src, pos + 1, acc, quote, at),
        Step::Byte(b) => append_and_continue(src, pos, b, acc, quote, at),
    }
}

fn append_and_continue(
    src: &[u8],
    pos: usize,
    byte: u8,
    acc: String,
    quote: u8,
    at: Position,
) -> Result<(String, usize), Error> {
    let next_acc = acc + &char::from(byte).to_string();
    scan_chars(src, pos + 1, next_acc, quote, at)
}

fn handle_escape(
    src: &[u8],
    pos: usize,
    acc: String,
    quote: u8,
    at: Position,
) -> Result<(String, usize), Error> {
    match peek(src, pos) {
        Step::End => Err(Error::UnterminatedString { at }),
        Step::Byte(b'n') => append_and_continue_with(src, pos + 1, '\n', acc, quote, at),
        Step::Byte(b't') => append_and_continue_with(src, pos + 1, '\t', acc, quote, at),
        Step::Byte(b'r') => append_and_continue_with(src, pos + 1, '\r', acc, quote, at),
        Step::Byte(b'\\') => append_and_continue_with(src, pos + 1, '\\', acc, quote, at),
        Step::Byte(b'\'') => append_and_continue_with(src, pos + 1, '\'', acc, quote, at),
        Step::Byte(b'"') => append_and_continue_with(src, pos + 1, '"', acc, quote, at),
        Step::Byte(b'`') => append_and_continue_with(src, pos + 1, '`', acc, quote, at),
        Step::Byte(b'0') => handle_null_escape(src, pos + 1, acc, quote, at),
        Step::Byte(b'b') => append_and_continue_with(src, pos + 1, '\u{08}', acc, quote, at),
        Step::Byte(b'f') => append_and_continue_with(src, pos + 1, '\u{0c}', acc, quote, at),
        Step::Byte(b'v') => append_and_continue_with(src, pos + 1, '\u{0b}', acc, quote, at),
        Step::Byte(b'x') => handle_hex_escape(src, pos + 1, acc, quote, at),
        Step::Byte(b'u') => handle_unicode_escape(src, pos + 1, acc, quote, at),
        Step::Byte(b'\n') => scan_chars(src, pos + 1, acc, quote, at),
        Step::Byte(b'\r') => scan_chars(src, skip_optional_lf(src, pos + 1), acc, quote, at),
        Step::Byte(other) => {
            append_and_continue_with(src, pos + 1, char::from(other), acc, quote, at)
        }
    }
}

fn append_and_continue_with(
    src: &[u8],
    next_pos: usize,
    ch: char,
    acc: String,
    quote: u8,
    at: Position,
) -> Result<(String, usize), Error> {
    let next_acc = acc + &ch.to_string();
    scan_chars(src, next_pos, next_acc, quote, at)
}

fn handle_null_escape(
    src: &[u8],
    pos: usize,
    acc: String,
    quote: u8,
    at: Position,
) -> Result<(String, usize), Error> {
    // `\0` is the null character unless followed by a digit (legacy octal).
    match peek(src, pos) {
        Step::Byte(b'0'..=b'9') => Err(Error::InvalidEscape {
            at,
            reason: "legacy octal escapes are not supported",
        }),
        Step::End | Step::Byte(_) => append_and_continue_with(src, pos, '\0', acc, quote, at),
    }
}

fn handle_hex_escape(
    src: &[u8],
    pos: usize,
    acc: String,
    quote: u8,
    at: Position,
) -> Result<(String, usize), Error> {
    let hi = src
        .get(pos)
        .copied()
        .filter(u8::is_ascii_hexdigit)
        .ok_or(Error::InvalidEscape {
            at,
            reason: "\\x needs two hex digits",
        })?;
    let lo = src
        .get(pos + 1)
        .copied()
        .filter(u8::is_ascii_hexdigit)
        .ok_or(Error::InvalidEscape {
            at,
            reason: "\\x needs two hex digits",
        })?;
    let value: u32 = hex_value(hi) * 16 + hex_value(lo);
    let ch = char::from_u32(value).ok_or(Error::InvalidUnicodeEscape { at })?;
    append_and_continue_with(src, pos + 2, ch, acc, quote, at)
}

fn handle_unicode_escape(
    src: &[u8],
    pos: usize,
    acc: String,
    quote: u8,
    at: Position,
) -> Result<(String, usize), Error> {
    match peek(src, pos) {
        Step::Byte(b'{') => handle_unicode_braced(src, pos + 1, acc, quote, at),
        Step::End | Step::Byte(_) => handle_unicode_four(src, pos, acc, quote, at),
    }
}

fn handle_unicode_four(
    src: &[u8],
    pos: usize,
    acc: String,
    quote: u8,
    at: Position,
) -> Result<(String, usize), Error> {
    let d0 = digit_or_err(src, pos, at)?;
    let d1 = digit_or_err(src, pos + 1, at)?;
    let d2 = digit_or_err(src, pos + 2, at)?;
    let d3 = digit_or_err(src, pos + 3, at)?;
    let value = ((d0 * 16 + d1) * 16 + d2) * 16 + d3;
    let ch = char::from_u32(value).ok_or(Error::InvalidUnicodeEscape { at })?;
    append_and_continue_with(src, pos + 4, ch, acc, quote, at)
}

fn handle_unicode_braced(
    src: &[u8],
    pos: usize,
    acc: String,
    quote: u8,
    at: Position,
) -> Result<(String, usize), Error> {
    let end = scan_braced_digits(src, pos);
    if end == pos {
        Err(Error::InvalidEscape {
            at,
            reason: "\\u{} needs at least one hex digit",
        })
    } else if src.get(end).copied() != Some(b'}') {
        Err(Error::InvalidEscape {
            at,
            reason: "\\u{ ... } needs a closing brace",
        })
    } else {
        let value = src
            .get(pos..end)
            .unwrap_or(&[])
            .iter()
            .fold(0_u32, |acc_value, &b| acc_value * 16 + hex_value(b));
        let ch = char::from_u32(value).ok_or(Error::InvalidUnicodeEscape { at })?;
        append_and_continue_with(src, end + 1, ch, acc, quote, at)
    }
}

fn scan_braced_digits(src: &[u8], from: usize) -> usize {
    src.get(from)
        .copied()
        .filter(u8::is_ascii_hexdigit)
        .map_or(from, |_| scan_braced_digits(src, from + 1))
}

fn digit_or_err(src: &[u8], pos: usize, at: Position) -> Result<u32, Error> {
    src.get(pos)
        .copied()
        .filter(u8::is_ascii_hexdigit)
        .map(hex_value)
        .ok_or(Error::InvalidEscape {
            at,
            reason: "\\uNNNN needs four hex digits",
        })
}

fn skip_optional_lf(src: &[u8], pos: usize) -> usize {
    if src.get(pos).copied() == Some(b'\n') {
        pos + 1
    } else {
        pos
    }
}

fn hex_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,
    }
}
