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

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

use super::value::Value;

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

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

    kind: InterpretErrorKind,

    message: Option<String>,

    #[help]
    help: Option<String>,

    #[related]
    related: Vec<Box<dyn miette::Diagnostic + Send + Sync + 'static>>,
}

/// Interpret error kind.
#[derive(Debug, Diagnostic, Error)]
pub enum InterpretErrorKind {
    #[error("Cannot add types '{0}' and '{1}'")]
    InvalidAddition(&'static str, &'static str),
    #[error("Cannot subtract types '{1}' from '{0}'")]
    InvalidSubtraction(&'static str, &'static str),
    #[error("Cannot multiply types '{0}' and '{1}'")]
    InvalidMultiplication(&'static str, &'static str),
    #[error("Cannot divide types '{0}' by '{1}'")]
    InvalidDivision(&'static str, &'static str),
    #[error("Cannot perform modulo on types '{0}' and '{1}'")]
    InvalidModulo(&'static str, &'static str),
    #[error("Division by zero")]
    DivisionByZero,
    #[error("Cannot compare types '{0}' and '{1}'")]
    InvalidComparison(&'static str, &'static str),
    #[error("Cannot perform logical NOT on type '{0}'")]
    InvalidLogicalNot(&'static str),
    #[error("Cannot apply unary '+' to type '{0}'")]
    InvalidUnaryPlus(&'static str),
    #[error("Cannot perform logical AND on types '{0}' and '{1}'")]
    InvalidLogicalAnd(&'static str, &'static str),
    #[error("Cannot perform logical OR on types '{0}' and '{1}'")]
    InvalidLogicalOr(&'static str, &'static str),
    #[error("Duplicate declaration of '{0}'")]
    DuplicateDeclaration(String),
    #[error("Type mismatch for '{0}'")]
    TypeMismatch(String),
    #[error("Undefined identifier '{0}'")]
    UndefinedIdentifier(String),
    #[error("Variable '{0}' is used before it has been initialized")]
    UninitializedRead(String),
    #[error("Cannot assign to constant '{0}'")]
    AssignToConstant(String),
    #[error("Cannot assign to immutable variable '{0}'")]
    AssignToImmutable(String),
    #[error("Condition must be a boolean")]
    ConditionNotBoolean,
    #[error("Expected a value")]
    ExpectedValue,
    #[error("For loop iterable must be a range or array")]
    ForIterableNotIterable,
    #[error("Range bound must be a number")]
    RangeBoundNotNumeric,
    #[error("Array index must be a non-negative integer")]
    IndexNotInteger,
    #[error("Array index {index} out of bounds for array of length {length}")]
    IndexOutOfBounds { index: i64, length: usize },
    #[error("Cannot index into type '{0}'")]
    IndexOnNonArray(String),
    #[error("Invalid reference target")]
    InvalidRefTarget,
    #[error("Loop exceeded maximum iteration limit")]
    LoopIterationLimitExceeded(u64),
    #[error("Orphaned reference to '{0}'")]
    OrphanedReference(String),
    #[error("Array element type mismatch")]
    ArrayElementTypeMismatch,
    #[error("Array size mismatch: expected {expected} element(s), got {got}")]
    ArraySizeMismatch { expected: usize, got: usize },
    #[error("Array size {requested} exceeds limit of {limit}")]
    ArraySizeLimitExceeded { requested: usize, limit: usize },
    #[error("Assertion failed{}", .0.as_ref().map_or(String::new(), |m| format!(": {m}")))]
    AssertionFailed(Option<String>),
    #[error("Arity mismatch: expected {expected} argument(s), got {got}")]
    ArityMismatch { expected: usize, got: usize },
    #[error(
        "Method arity mismatch: '{type_name}.{method}()' expects {expected} argument(s), got {got}"
    )]
    MethodArityMismatch {
        type_name: String,
        method: String,
        expected: usize,
        got: usize,
    },
    #[error("'break' outside of loop")]
    BreakSignal(Value),
    #[error("Block nesting depth limit ({0}) exceeded")]
    BlockDepthExceeded(u64),
    #[error("Expression evaluation depth limit ({0}) exceeded")]
    ExpressionDepthExceeded(u64),
    #[error("Maximum call depth exceeded")]
    CallDepthExceeded(u64),
    #[error("'continue' outside of loop")]
    ContinueSignal,
    #[error("Value of type '{0}' is not callable")]
    NotCallable(String),
    #[error("Return type mismatch for function '{0}'")]
    ReturnTypeMismatch(String),
    #[error("'return' outside of function")]
    ReturnSignal(Value),
    #[error("Undefined method '{method}' on type '{type_name}'")]
    UndefinedMethod { type_name: String, method: String },
    #[error("Type '{type_name}' has no field '{field}'")]
    UndefinedField { type_name: String, field: String },
    #[error("Node type '{type_name}' has no port '{port}'")]
    UndefinedPort { type_name: String, port: String },
    #[error("Node type name '{0}' cannot be used as a value")]
    NodeTypeNotAValue(String),
    #[error("Identifier '{0}' is not a node type")]
    NotANodeType(String),
    #[error("{side}-hand side of '->' is not a port reference (got '{type_name}')")]
    NotAPortRef {
        side: &'static str,
        type_name: String,
    },
    #[error(
        "Cannot connect '{from_dir}' port '{from_node}.{from_port}' to '{to_dir}' port '{to_node}.{to_port}'"
    )]
    InvalidConnectionDirection {
        from_node: String,
        from_port: String,
        from_dir: String,
        to_node: String,
        to_port: String,
        to_dir: String,
    },
    #[error(
        "Duplicate connection: '{from_node}.{from_port}' -> '{to_node}.{to_port}' already exists"
    )]
    DuplicateConnection {
        from_node: String,
        from_port: String,
        to_node: String,
        to_port: String,
    },
    #[error("'self' can only be used inside a 'group {{ ... }}' body")]
    SelfOutsideGroup,
    #[error("'self.{name} = ...' is only valid inside a 'group {{ ... }}' body")]
    MemberDeclOutsideGroup { name: String },
    #[error("Member binding 'self.{name}' requires a node or group value, got '{type_name}'")]
    InvalidMember { name: String, type_name: String },
    #[error("Member 'self.{name}' is already bound and cannot be reassigned")]
    MemberReassignment { name: String },
    #[error("Value already belongs to '{existing_owner}'; cannot bind it again as 'self.{name}'")]
    DuplicateMembership {
        name: String,
        existing_owner: String,
    },
    #[error("'port' declarations are only valid inside a 'group {{ ... }}' body")]
    PortDeclOutsideGroup,
    #[error("Duplicate port '{name}' in group")]
    DuplicatePort { name: String },
    #[error("'port' declaration expression must evaluate to a port reference, got '{type_name}'")]
    PortDeclNotAPortRef { type_name: String },
    #[error("Port '{port}' target directions are inconsistent: expected {expected:?}, got {got:?}")]
    PortDeclDirectionMismatch {
        port: String,
        expected: PortDirection,
        got: PortDirection,
    },
    #[error("Error in imported file '{0}'")]
    ImportError(String),
    #[error("Import name collision: '{0}' is already defined")]
    ImportNameCollision(String),
    #[error(
        "Multiple top-level result expressions; only one trailing semicolon-less expression is allowed"
    )]
    MultipleTopLevelResults,
}

impl InterpretError {
    /// Get the error kind.
    #[must_use]
    pub fn kind(&self) -> &InterpretErrorKind {
        &self.kind
    }

    /// Create a new interpret error.
    #[must_use]
    pub fn new(
        source_code: NamedSource<String>,
        loc: &Location,
        kind: InterpretErrorKind,
        message: Option<impl Into<String>>,
        help: Option<impl Into<String>>,
        length: Option<usize>,
    ) -> Self {
        let span = match &kind {
            InterpretErrorKind::UndefinedIdentifier(name)
            | InterpretErrorKind::AssignToConstant(name)
            | InterpretErrorKind::AssignToImmutable(name) => {
                (loc.as_source_offset(source_code.inner()), name.len())
            }
            _ => (
                loc.as_source_offset(source_code.inner()),
                length.unwrap_or(1),
            ),
        };
        Self {
            source_code,
            span: span.into(),
            kind,
            message: message.map(Into::into),
            help: help.map(Into::into),
            related: Vec::new(),
        }
    }

    /// Attach a related diagnostic (e.g. original scan/parse error from an imported file).
    #[must_use]
    pub fn with_related(mut self, diag: impl miette::Diagnostic + Send + Sync + 'static) -> Self {
        self.related.push(Box::new(diag));
        self
    }
}
