// Copyright © 2024-2025 The µcad authors <info@ucad.xyz>
// SPDX-License-Identifier: AGPL-3.0-or-later

use crate::{diag::*, resolve::*, src_ref::*};

/// Diagnostic message with source code reference attached.
pub enum Diagnostic {
    /// Trace message.
    Trace(Refer<String>),
    /// Informative message.
    Info(Refer<String>),
    /// Warning.
    Warning(Refer<Box<dyn std::error::Error>>),
    /// Error.
    Error(Refer<Box<dyn std::error::Error>>),
}

impl Diagnostic {
    /// Get diagnostic level.
    pub fn level(&self) -> Level {
        match self {
            Diagnostic::Trace(_) => Level::Trace,
            Diagnostic::Info(_) => Level::Info,
            Diagnostic::Warning(_) => Level::Warning,
            Diagnostic::Error(_) => Level::Error,
        }
    }

    /// Get message (errors will be serialized).
    pub fn message(&self) -> String {
        match self {
            Diagnostic::Trace(msg) | Diagnostic::Info(msg) => msg.to_string(),
            Diagnostic::Warning(err) | Diagnostic::Error(err) => err.to_string(),
        }
    }

    /// Return line of the error
    pub fn line(&self) -> Option<usize> {
        let src_ref = match self {
            Diagnostic::Trace(r) => r.src_ref(),
            Diagnostic::Info(r) => r.src_ref(),
            Diagnostic::Warning(r) => r.src_ref(),
            Diagnostic::Error(r) => r.src_ref(),
        };
        src_ref.as_ref().map(|r| r.at.line)
    }

    /// Pretty print the diagnostic.
    ///
    /// This will print the diagnostic to the given writer, including the source code reference.
    ///
    /// # Arguments
    ///
    /// * `w` - The writer to write to.
    /// * `source_file_by_hash` - Hash provider to get the source file by hash.
    ///
    /// This will print:
    ///
    /// ```text
    /// error: This is an error
    ///   ---> filename:1:8
    ///     |
    ///  1  | part Circle(radius: length) {}
    ///     |        ^^^^^^
    /// ```
    pub fn pretty_print(
        &self,
        f: &mut dyn std::fmt::Write,
        source_by_hash: &impl GetSourceByHash,
        line_offset: usize,
    ) -> std::fmt::Result {
        let src_ref = self.src_ref();

        let source_file = source_by_hash.get_by_hash(src_ref.source_hash());

        fn make_relative(path: &std::path::Path) -> String {
            let current_dir = std::env::current_dir().expect("current dir");
            if let Ok(path) = path.canonicalize() {
                pathdiff::diff_paths(path, current_dir)
                    .expect("related paths:\n  {path:?}\n  {current_dir:?}")
            } else {
                path.to_path_buf()
            }
            .to_string_lossy()
            .to_string()
        }
        match &src_ref {
            SrcRef(None) => writeln!(f, "{}: {}", self.level(), self.message())?,
            SrcRef(Some(src_ref)) => {
                writeln!(f, "{}: {}", self.level(), self.message())?;
                writeln!(
                    f,
                    "  ---> {}:{}",
                    source_file
                        .as_ref()
                        .map(|sf| make_relative(&sf.filename()))
                        .unwrap_or(crate::invalid_no_ansi!(FILE).to_string()),
                    src_ref.with_line_offset(line_offset).at
                )?;
                writeln!(f, "     |",)?;

                let line = source_file
                    .as_ref()
                    .map(|sf| {
                        sf.get_line(src_ref.at.line - 1)
                            .unwrap_or(crate::invalid!(LINE))
                    })
                    .unwrap_or(crate::invalid_no_ansi!(FILE));

                writeln!(
                    f,
                    "{: >4} | {}",
                    src_ref.with_line_offset(line_offset).at.line,
                    line
                )?;
                writeln!(
                    f,
                    "{: >4} | {}",
                    "",
                    " ".repeat(src_ref.at.col - 1)
                        + &"^".repeat(src_ref.range.len().min(line.len())),
                )?;
                writeln!(f, "     |",)?;
            }
        }

        Ok(())
    }
}

impl SrcReferrer for Diagnostic {
    fn src_ref(&self) -> SrcRef {
        match self {
            Diagnostic::Trace(message) => message.src_ref(),
            Diagnostic::Info(message) => message.src_ref(),
            Diagnostic::Warning(error) => error.src_ref(),
            Diagnostic::Error(error) => error.src_ref(),
        }
    }
}

impl std::fmt::Display for Diagnostic {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Diagnostic::Trace(message) => write!(f, "trace: {}: {message}", self.src_ref()),
            Diagnostic::Info(message) => write!(f, "info: {}: {message}", self.src_ref()),
            Diagnostic::Warning(error) => write!(f, "warning: {}: {error}", self.src_ref()),
            Diagnostic::Error(error) => write!(f, "error: {}: {error}", self.src_ref()),
        }
    }
}

impl std::fmt::Debug for Diagnostic {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Diagnostic::Trace(message) => write!(f, "trace: {}: {message}", self.src_ref()),
            Diagnostic::Info(message) => write!(f, "info: {}: {message}", self.src_ref()),
            Diagnostic::Warning(error) => write!(f, "warning: {}: {error:?}", self.src_ref()),
            Diagnostic::Error(error) => write!(f, "error: {}: {error:?}", self.src_ref()),
        }
    }
}
