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

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

/// Parse result error.
#[derive(Clone, Debug, Error, PartialEq, Diagnostic)]
#[error("{kind}{}", .message.as_ref().map(|m| format!(": {m}")).unwrap_or_default())]
pub struct ParseError {
    #[source_code]
    source_code: NamedSource<String>,

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

    kind: ParseErrorKind,

    message: Option<String>,

    #[related]
    related: Vec<ParseError>,

    #[help]
    help: Option<String>,
}

/// Scan error kind.
#[derive(Clone, Debug, Diagnostic, Error, PartialEq)]
pub enum ParseErrorKind {
    #[error("End of stream (expected)")]
    EndOfStream,
    #[error("Invalid assignment target")]
    InvalidAssignmentTarget,
    #[error("Invalid constant name '{0}'")]
    InvalidConstantName(String),
    #[error("Invalid statement")]
    InvalidStatement,
    #[error("'loop' without 'break'")]
    LoopWithoutBreak,
    #[error("Missing semicolon after '{0}' statement")]
    MissingSemicolonAfterStatement(String),
    #[error("Duplicate port name '{0}'")]
    DuplicatePortName(String),
    #[error("'node type' declaration is only allowed at file top level")]
    NodeTypeDeclNotTopLevel,
    #[error("'port' declarations are only valid inside a 'group {{ ... }}' body")]
    PortDeclOutsideGroup,
    #[error("Unexpected end-of-stream")]
    UnexpectedEndOfStream,
    #[error("Unexpected token")]
    UnexpectedToken,
    #[error("Unexpected trailing token")]
    UnexpectedTrailingToken,
    #[error("Expression nesting depth limit ({0}) exceeded")]
    ExpressionDepthLimitExceeded(u64),
    #[error("'use' declarations are only valid at file top level")]
    UseDeclNotTopLevel,
}

impl ParseError {
    /// Create a new parse error.
    #[must_use]
    pub fn new(
        source_code: NamedSource<String>,
        loc: &Location,
        kind: ParseErrorKind,
        message: Option<impl Into<String>>,
        help: Option<impl Into<String>>,
        length: Option<usize>,
    ) -> Self {
        let span = match &kind {
            ParseErrorKind::MissingSemicolonAfterStatement(s) => {
                (loc.as_source_offset(source_code.inner()), s.len())
            }
            _ => (
                loc.as_source_offset(source_code.inner()),
                length.unwrap_or(0),
            ),
        };
        Self {
            source_code,
            span: span.into(),
            kind,
            message: message.map(Into::into),
            related: Vec::new(),
            help: help.map(Into::into),
        }
    }

    /// Create a new parse error with a related error.
    pub fn with_related(
        source_code: NamedSource<String>,
        loc: &Location,
        kind: ParseErrorKind,
        message: Option<impl Into<String>>,
        related: ParseError,
        help: Option<impl Into<String>>,
        length: Option<usize>,
    ) -> Self {
        let mut error = Self::new(source_code, loc, kind, message, help, length);
        error.related.push(related);
        error
    }

    /// Get the kind of the parse error.
    #[must_use]
    pub fn kind(&self) -> &ParseErrorKind {
        &self.kind
    }
}
