//! Error data types and utilities for handling/reporting them.
//!
//! The main type in this module is [`OxcDiagnostic`], which is used by all other oxc tools to
//! report problems. It implements [miette]'s [`Diagnostic`] trait, making it compatible with other
//! tooling you may be using.
//!
//! ```rust,ignore
//! use oxc_diagnostics::{OxcDiagnostic, Result};
//! fn my_tool() -> Result<()> {
//!     try_something().map_err(|e| OxcDiagnostic::error(e.to_string()))?;
//!     Ok(())
//! }
//! ```
//!
//! See the [miette] documentation for more information on how to interact with diagnostics.
//!
//! ## Reporting
//! If you are writing your own tools that may produce their own errors, you can use
//! [`DiagnosticService`] to format and render them to a string or a stream. It can receive
//! [`Error`]s over a multi-producer, single consumer
//!
//! ```rust,ignore
//! use std::{path::PathBuf, sync::Arc, thread};
//! use oxc_diagnostics::{DiagnosticService, Error, OxcDiagnostic, GraphicalReportHandler, NamedSource};
//!
//! fn my_tool() -> Result<()> {
//!     try_something().map_err(|e| OxcDiagnostic::error(e.to_string()))?;
//!     Ok(())
//! }
//!
//! let (mut service, sender) = DiagnosticService::new(Box::new(GraphicalReportHandler::new()));
//!
//! thread::spawn(move || {
//!     let file_path_being_processed = PathBuf::from("file.txt");
//!     let file_being_processed = Arc::new(NamedSource::new(file_path_being_processed.clone()));
//!
//!     for _ in 0..10 {
//!         if let Err(diagnostic) = my_tool() {
//!             let report = diagnostic.with_source_code(Arc::clone(&file_being_processed));
//!             sender.send((file_path_being_processed, vec![Error::new(e)]));
//!         }
//!         // The service will stop when all senders are dropped
//!     }
//! });
//!
//! service.run();
//! ```

mod service;

use std::{
    borrow::Cow,
    fmt::{self, Display},
    ops::{Deref, DerefMut},
};

pub mod reporter;

pub use crate::service::{DiagnosticSender, DiagnosticService};

pub type Error = miette::Error;
pub type Severity = miette::Severity;

pub type Result<T> = std::result::Result<T, OxcDiagnostic>;

use miette::{Diagnostic, SourceCode};
pub use miette::{GraphicalReportHandler, GraphicalTheme, LabeledSpan, Labels, NamedSource};

/// A collection of [`OxcDiagnostic`]s.
///
/// A drop-in replacement for `Vec<OxcDiagnostic>` that can additionally report
/// whether it holds any [errors](Severity::Error) or [warnings](Severity::Warning).
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Diagnostics(Vec<OxcDiagnostic>);

impl Diagnostics {
    pub fn new() -> Self {
        Self(Vec::new())
    }

    /// `true` if any diagnostic has [error](Severity::Error) severity.
    pub fn has_errors(&self) -> bool {
        self.0.iter().any(|diagnostic| diagnostic.severity == Severity::Error)
    }

    /// `true` if any diagnostic has [warning](Severity::Warning) severity.
    pub fn has_warnings(&self) -> bool {
        self.0.iter().any(|diagnostic| diagnostic.severity == Severity::Warning)
    }

    /// Iterate over the [error](Severity::Error)-severity diagnostics.
    pub fn errors(&self) -> impl Iterator<Item = &OxcDiagnostic> {
        self.0.iter().filter(|diagnostic| diagnostic.severity == Severity::Error)
    }

    /// Iterate over the [warning](Severity::Warning)-severity diagnostics.
    pub fn warnings(&self) -> impl Iterator<Item = &OxcDiagnostic> {
        self.0.iter().filter(|diagnostic| diagnostic.severity == Severity::Warning)
    }

    pub fn into_vec(self) -> Vec<OxcDiagnostic> {
        self.0
    }

    pub fn push(&mut self, diagnostic: OxcDiagnostic) {
        self.0.push(diagnostic);
    }

    pub fn extend(&mut self, diagnostics: impl IntoIterator<Item = OxcDiagnostic>) {
        self.0.extend(diagnostics);
    }
}

impl Deref for Diagnostics {
    type Target = Vec<OxcDiagnostic>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl DerefMut for Diagnostics {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

impl From<Vec<OxcDiagnostic>> for Diagnostics {
    fn from(diagnostics: Vec<OxcDiagnostic>) -> Self {
        Self(diagnostics)
    }
}

impl From<OxcDiagnostic> for Diagnostics {
    fn from(diagnostic: OxcDiagnostic) -> Self {
        Self(vec![diagnostic])
    }
}

impl From<Diagnostics> for Vec<OxcDiagnostic> {
    fn from(diagnostics: Diagnostics) -> Self {
        diagnostics.0
    }
}

impl FromIterator<OxcDiagnostic> for Diagnostics {
    fn from_iter<T: IntoIterator<Item = OxcDiagnostic>>(iter: T) -> Self {
        Self(iter.into_iter().collect())
    }
}

impl IntoIterator for Diagnostics {
    type Item = OxcDiagnostic;
    type IntoIter = std::vec::IntoIter<OxcDiagnostic>;

    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

impl<'a> IntoIterator for &'a Diagnostics {
    type Item = &'a OxcDiagnostic;
    type IntoIter = std::slice::Iter<'a, OxcDiagnostic>;

    fn into_iter(self) -> Self::IntoIter {
        self.0.iter()
    }
}

/// Describes an error or warning that occurred.
///
/// Used by all oxc tools.
#[derive(Debug, Clone, Eq, PartialEq)]
#[must_use]
pub struct OxcDiagnostic {
    inner: OxcDiagnosticInner,
}

impl Deref for OxcDiagnostic {
    type Target = OxcDiagnosticInner;

    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

impl DerefMut for OxcDiagnostic {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.inner
    }
}

#[derive(Debug, Default, Clone, Eq, PartialEq, PartialOrd, Ord)]
pub struct OxcCode {
    pub scope: Option<Cow<'static, str>>,
    pub number: Option<Cow<'static, str>>,
}

impl OxcCode {
    pub fn is_some(&self) -> bool {
        self.scope.is_some() || self.number.is_some()
    }
}

impl Display for OxcCode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match (&self.scope, &self.number) {
            (Some(scope), Some(number)) => write!(f, "{scope}({number})"),
            (Some(scope), None) => scope.fmt(f),
            (None, Some(number)) => number.fmt(f),
            (None, None) => Ok(()),
        }
    }
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct OxcDiagnosticInner {
    pub message: Cow<'static, str>,
    pub labels: Labels,
    pub help: Option<Cow<'static, str>>,
    pub note: Option<Cow<'static, str>>,
    pub severity: Severity,
    pub code: OxcCode,
    pub url: Option<Cow<'static, str>>,
}

impl Display for OxcDiagnostic {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
        self.message.fmt(f)
    }
}

impl std::error::Error for OxcDiagnostic {}

impl Diagnostic for OxcDiagnostic {
    /// The secondary help message.
    fn help(&self) -> Option<Cow<'_, str>> {
        self.help.as_deref().map(Cow::Borrowed)
    }

    /// A note for the diagnostic.
    ///
    /// Similar to rustc - intended for additional explanation and information,
    /// e.g. why an error was emitted, how to turn it off.
    fn note(&self) -> Option<Cow<'_, str>> {
        self.note.as_deref().map(Cow::Borrowed)
    }

    /// The severity level of this diagnostic.
    ///
    /// Diagnostics with missing severity levels should be treated as [errors](Severity::Error).
    fn severity(&self) -> Option<Severity> {
        Some(self.severity)
    }

    /// Labels covering problematic portions of source code.
    fn labels(&self) -> Labels {
        self.labels.clone()
    }

    /// An error code uniquely identifying this diagnostic.
    ///
    /// Note that codes may be scoped, which will be rendered as `scope(code)`.
    fn code(&self) -> Option<Cow<'_, str>> {
        self.code.is_some().then(|| Cow::Owned(self.code.to_string()))
    }

    /// A URL that provides more information about the problem that occurred.
    fn url(&self) -> Option<Cow<'_, str>> {
        self.url.as_deref().map(Cow::Borrowed)
    }
}

impl OxcDiagnostic {
    /// Create new an error-level [`OxcDiagnostic`].
    pub fn error<T: Into<Cow<'static, str>>>(message: T) -> Self {
        Self {
            inner: OxcDiagnosticInner {
                message: message.into(),
                labels: Labels::None,
                note: None,
                help: None,
                severity: Severity::Error,
                code: OxcCode::default(),
                url: None,
            },
        }
    }

    /// Create new a warning-level [`OxcDiagnostic`].
    pub fn warn<T: Into<Cow<'static, str>>>(message: T) -> Self {
        Self {
            inner: OxcDiagnosticInner {
                message: message.into(),
                labels: Labels::None,
                help: None,
                note: None,
                severity: Severity::Warning,
                code: OxcCode::default(),
                url: None,
            },
        }
    }

    /// Add a scoped error code to this diagnostic.
    ///
    /// This is a shorthand for `with_error_code_scope(scope).with_error_code_num(number)`.
    #[inline]
    pub fn with_error_code<T: Into<Cow<'static, str>>, U: Into<Cow<'static, str>>>(
        self,
        scope: T,
        number: U,
    ) -> Self {
        self.with_error_code_scope(scope).with_error_code_num(number)
    }

    /// Add an error code scope to this diagnostic.
    ///
    /// Use [`OxcDiagnostic::with_error_code`] to set both the scope and number at once.
    #[inline]
    pub fn with_error_code_scope<T: Into<Cow<'static, str>>>(mut self, code_scope: T) -> Self {
        self.inner.code.scope = match self.inner.code.scope {
            Some(scope) => Some(scope),
            None => Some(code_scope.into()),
        };
        debug_assert!(
            self.inner.code.scope.as_ref().is_some_and(|s| !s.is_empty()),
            "Error code scopes cannot be empty"
        );

        self
    }

    /// Add an error code number to this diagnostic.
    ///
    /// Use [`OxcDiagnostic::with_error_code`] to set both the scope and number at once.
    #[inline]
    pub fn with_error_code_num<T: Into<Cow<'static, str>>>(mut self, code_num: T) -> Self {
        self.inner.code.number = match self.inner.code.number {
            Some(num) => Some(num),
            None => Some(code_num.into()),
        };
        debug_assert!(
            self.inner.code.number.as_ref().is_some_and(|n| !n.is_empty()),
            "Error code numbers cannot be empty"
        );

        self
    }

    /// Set the severity level of this diagnostic.
    ///
    /// Use [`OxcDiagnostic::error`] or [`OxcDiagnostic::warn`] to create a diagnostic at the
    /// severity you want.
    pub fn with_severity(mut self, severity: Severity) -> Self {
        self.inner.severity = severity;
        self
    }

    /// Suggest a possible solution for a problem to the user.
    ///
    /// ## Example
    /// ```rust,ignore
    /// use std::path::PathBuf;
    /// use oxc_diagnostics::OxcDiagnostic
    ///
    /// let config_file_path = Path::from("config.json");
    /// if !config_file_path.exists() {
    ///     return Err(OxcDiagnostic::error("No config file found")
    ///         .with_help("Run my_tool --init to set up a new config file"));
    /// }
    /// ```
    pub fn with_help<T: Into<Cow<'static, str>>>(mut self, help: T) -> Self {
        self.inner.help = Some(help.into());
        self
    }

    /// Show a note to the user.
    ///
    /// ## Example
    /// ```rust,ignore
    /// use std::path::PathBuf;
    /// use oxc_diagnostics::OxcDiagnostic
    ///
    /// let config_file_path = Path::from("config.json");
    /// if !config_file_path.exists() {
    ///     return Err(OxcDiagnostic::error("No config file found")
    ///         .with_help("Run my_tool --init to set up a new config file")
    ///         .with_note("Some useful information or suggestion"));
    /// }
    /// ```
    pub fn with_note<T: Into<Cow<'static, str>>>(mut self, note: T) -> Self {
        self.inner.note = Some(note.into());
        self
    }

    /// Set the label covering a problematic portion of source code.
    ///
    /// Existing labels will be removed. Use [`OxcDiagnostic::and_label`] append a label instead.
    ///
    /// You need to add some source code to this diagnostic (using
    /// [`OxcDiagnostic::with_source_code`]) for this to actually be useful. Use
    /// [`OxcDiagnostic::with_labels`] to add multiple labels all at once.
    ///
    /// Note that this pairs nicely with [`oxc_span::Span`], particularly the [`label`] method.
    ///
    /// [`oxc_span::Span`]: https://docs.rs/oxc_span/latest/oxc_span/struct.Span.html
    /// [`label`]: https://docs.rs/oxc_span/latest/oxc_span/struct.Span.html#method.label
    pub fn with_label<T: Into<LabeledSpan>>(mut self, label: T) -> Self {
        self.inner.labels = Labels::One([label.into()]);
        self
    }

    /// Add multiple labels covering problematic portions of source code.
    ///
    /// Existing labels will be removed. Use [`OxcDiagnostic::and_labels`] to append labels
    /// instead.
    ///
    /// You need to add some source code using [`OxcDiagnostic::with_source_code`] for this to
    /// actually be useful. If you only have a single label, consider using
    /// [`OxcDiagnostic::with_label`] instead.
    ///
    /// Note that this pairs nicely with [`oxc_span::Span`], particularly the [`label`] method.
    ///
    /// [`oxc_span::Span`]: https://docs.rs/oxc_span/latest/oxc_span/struct.Span.html
    /// [`label`]: https://docs.rs/oxc_span/latest/oxc_span/struct.Span.html#method.label
    pub fn with_labels<L: Into<LabeledSpan>, T: IntoIterator<Item = L>>(
        mut self,
        labels: T,
    ) -> Self {
        self.inner.labels = labels.into_iter().map(Into::into).collect();
        self
    }

    /// Add a label to this diagnostic without clobbering existing labels.
    pub fn and_label<T: Into<LabeledSpan>>(mut self, label: T) -> Self {
        self.inner.labels.push(label.into());
        self
    }

    /// Add multiple labels to this diagnostic without clobbering existing labels.
    pub fn and_labels<L: Into<LabeledSpan>, T: IntoIterator<Item = L>>(
        mut self,
        labels: T,
    ) -> Self {
        self.inner.labels.extend(labels.into_iter().map(Into::into));
        self
    }

    /// Add a URL that provides more information about this diagnostic.
    pub fn with_url<S: Into<Cow<'static, str>>>(mut self, url: S) -> Self {
        self.inner.url = Some(url.into());
        self
    }

    /// Add source code to this diagnostic and convert it into an [`Error`].
    ///
    /// You should use a [`NamedSource`] if you have a file name as well as the source code.
    pub fn with_source_code<T: SourceCode + Send + Sync + 'static>(self, code: T) -> Error {
        Error::from(self).with_source_code(code)
    }

    /// Consumes the diagnostic and returns the inner owned data.
    pub fn inner_owned(self) -> OxcDiagnosticInner {
        self.inner
    }
}
