//! Template literal lexing.
//!
//! Template literals are lexed as a sequence of pieces: a literal text
//! chunk possibly followed by `${`, switching the lexer back into
//! expression mode until a matching `}` reopens the template stream.

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

/// One piece of a template literal as seen at its leading edge.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TemplatePiece {
    /// The whole template (no `${` interpolations).
    NoSubstitution(String),
    /// The leading slice up to (but not including) `${`.
    Head(String),
}

/// One piece of a template literal as seen after the closing brace of an
/// interpolated expression.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TemplateContinuation {
    /// A middle slice up to the next `${`.
    Middle(String),
    /// The trailing slice up to the closing backtick.
    Tail(String),
}

/// Scan a template literal starting at byte `start` (which points to the
/// opening backtick).
///
/// # Errors
///
/// Returns [`Error::UnterminatedTemplate`] on EOF before the closing
/// backtick or interpolation marker, or [`Error::InvalidEscape`] on a
/// malformed escape inside the template content.
pub fn scan_open(src: &[u8], start: usize, at: Position) -> Result<(TemplatePiece, usize), Error> {
    scan_until_boundary(src, start + 1, String::new(), at).map(|(content, end, has_subst)| {
        if has_subst {
            (TemplatePiece::Head(content), end)
        } else {
            (TemplatePiece::NoSubstitution(content), end)
        }
    })
}

/// Continue scanning a template after the closing `}` of an interpolated
/// expression.  `start` is the position of the `}`.
///
/// # Errors
///
/// Same as [`scan_open`].
pub fn scan_continue(
    src: &[u8],
    start: usize,
    at: Position,
) -> Result<(TemplateContinuation, usize), Error> {
    scan_until_boundary(src, start + 1, String::new(), at).map(|(content, end, has_subst)| {
        if has_subst {
            (TemplateContinuation::Middle(content), end)
        } else {
            (TemplateContinuation::Tail(content), end)
        }
    })
}

/// Scan template content, returning `(content, end_pos, has_substitution)`.
/// `end_pos` is the position just past the closing backtick (when
/// `has_substitution == false`) or just past the `${` (when `true`).
fn scan_until_boundary(
    src: &[u8],
    pos: usize,
    acc: String,
    at: Position,
) -> Result<(String, usize, bool), Error> {
    match src.get(pos).copied() {
        None => Err(Error::UnterminatedTemplate { at }),
        Some(b'`') => Ok((acc, pos + 1, false)),
        Some(b'$') if src.get(pos + 1).copied() == Some(b'{') => Ok((acc, pos + 2, true)),
        Some(b'\\') => handle_escape(src, pos + 1, acc, at),
        Some(b) => {
            let next_acc = acc + &char::from(b).to_string();
            scan_until_boundary(src, pos + 1, next_acc, at)
        }
    }
}

fn handle_escape(
    src: &[u8],
    pos: usize,
    acc: String,
    at: Position,
) -> Result<(String, usize, bool), Error> {
    // For template literals the escape table mirrors strings, but the
    // result is concatenated into the same accumulator.  Falling through to
    // verbatim-character semantics on unrecognised escapes matches the
    // ECMA-262 sloppy default (strict mode handling is a parser concern).
    match src.get(pos).copied() {
        None => Err(Error::UnterminatedTemplate { at }),
        Some(b'n') => continue_with(src, pos + 1, '\n', acc, at),
        Some(b't') => continue_with(src, pos + 1, '\t', acc, at),
        Some(b'r') => continue_with(src, pos + 1, '\r', acc, at),
        Some(b'\\') => continue_with(src, pos + 1, '\\', acc, at),
        Some(b'`') => continue_with(src, pos + 1, '`', acc, at),
        Some(b'$') => continue_with(src, pos + 1, '$', acc, at),
        Some(b'\'') => continue_with(src, pos + 1, '\'', acc, at),
        Some(b'"') => continue_with(src, pos + 1, '"', acc, at),
        Some(b'b') => continue_with(src, pos + 1, '\u{08}', acc, at),
        Some(b'f') => continue_with(src, pos + 1, '\u{0c}', acc, at),
        Some(b'v') => continue_with(src, pos + 1, '\u{0b}', acc, at),
        Some(other) => continue_with(src, pos + 1, char::from(other), acc, at),
    }
}

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