#![allow(unused_assignments)] // Fields are used by derive macros (miette, thiserror).

use crate::Location;
use miette::{Diagnostic, NamedSource, SourceOffset, SourceSpan};
use thiserror::Error;

/// Scan result error.
#[derive(Debug, Error, PartialEq, Diagnostic)]
#[error("{kind}")]
#[diagnostic(help("Failed to complete lexical scanning; review the input source code."))]
pub struct ScanError {
    #[source_code]
    source_code: NamedSource<String>,

    #[label("{kind}")]
    span: SourceSpan,

    kind: ScanErrorKind,
}

/// Scan error kind.
#[derive(Debug, Diagnostic, Error, PartialEq)]
pub enum ScanErrorKind {
    #[error("Invalid use of '_' in number literal '{0}'.")]
    InvalidNumberUnderscore(String),
    #[error("Malformed number literal '{0}', expecting fractional part.")]
    MalformedNumberLiteral(String),
    #[error("Unexpected character {0}.")]
    UnexpectedCharacter(char),
    #[error("Unterminated string literal.")]
    UnterminatedStringLiteral(String),
    #[error("Invalid escape sequence '\\{0}'.")]
    InvalidEscapeSequence(char),
    #[error("Invalid hex digit '{0}' in escape sequence.")]
    InvalidHexDigit(char),
    #[error("Incomplete escape sequence '\\{0}'.")]
    IncompleteEscapeSequence(char),
}

impl ScanError {
    /// Create a new scan error.
    #[must_use]
    pub fn new(source_code: NamedSource<String>, loc: &Location, kind: ScanErrorKind) -> Self {
        let span = match &kind {
            ScanErrorKind::InvalidNumberUnderscore(n)
            | ScanErrorKind::MalformedNumberLiteral(n) => {
                // Location points at the last consumed character; back-compute span start.
                (
                    SourceOffset::from_location(
                        source_code.inner(),
                        loc.line,
                        loc.column.saturating_sub(n.len()) + 1,
                    ),
                    n.len(),
                )
            }
            ScanErrorKind::UnterminatedStringLiteral(s) => (
                SourceOffset::from_location(
                    source_code.inner(),
                    loc.line,
                    loc.column.saturating_sub(s.chars().count()),
                ),
                s.len(),
            ),
            ScanErrorKind::InvalidEscapeSequence(_) => (
                SourceOffset::from_location(
                    source_code.inner(),
                    loc.line,
                    loc.column.saturating_sub(2),
                ),
                2,
            ),
            ScanErrorKind::InvalidHexDigit(_) => (
                SourceOffset::from_location(
                    source_code.inner(),
                    loc.line,
                    loc.column.saturating_sub(1),
                ),
                1,
            ),
            _ => (loc.as_source_offset(source_code.inner()), 1),
        };
        Self {
            source_code,
            span: span.into(),
            kind,
        }
    }
}
