use crate::{
    files::File,
    source::{line_index, source_text},
    Db,
};
use ruff_python_parser::ParseError;
use ruff_text_size::TextRange;
use std::borrow::Cow;

pub trait Diagnostic: Send + Sync + std::fmt::Debug {
    fn rule(&self) -> &str;

    fn message(&self) -> std::borrow::Cow<str>;

    fn file(&self) -> File;

    fn range(&self) -> Option<TextRange>;

    fn severity(&self) -> Severity;

    fn display<'a>(&'a self, db: &'a dyn Db) -> DisplayDiagnostic<'a>
    where
        Self: Sized,
    {
        DisplayDiagnostic {
            db,
            diagnostic: self,
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub enum Severity {
    Info,
    Error,
}

pub struct DisplayDiagnostic<'db> {
    db: &'db dyn Db,
    diagnostic: &'db dyn Diagnostic,
}

impl<'db> DisplayDiagnostic<'db> {
    pub fn new(db: &'db dyn Db, diagnostic: &'db dyn Diagnostic) -> Self {
        Self { db, diagnostic }
    }
}

impl std::fmt::Display for DisplayDiagnostic<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self.diagnostic.severity() {
            Severity::Info => f.write_str("info")?,
            Severity::Error => f.write_str("error")?,
        }

        write!(
            f,
            "[{rule}] {path}",
            rule = self.diagnostic.rule(),
            path = self.diagnostic.file().path(self.db)
        )?;

        if let Some(range) = self.diagnostic.range() {
            let index = line_index(self.db, self.diagnostic.file());
            let source = source_text(self.db, self.diagnostic.file());

            let start = index.source_location(range.start(), &source);

            write!(f, ":{line}:{col}", line = start.row, col = start.column)?;
        }

        write!(f, " {message}", message = self.diagnostic.message())
    }
}

impl<T> Diagnostic for Box<T>
where
    T: Diagnostic,
{
    fn rule(&self) -> &str {
        (**self).rule()
    }

    fn message(&self) -> Cow<str> {
        (**self).message()
    }

    fn file(&self) -> File {
        (**self).file()
    }

    fn range(&self) -> Option<TextRange> {
        (**self).range()
    }

    fn severity(&self) -> Severity {
        (**self).severity()
    }
}

impl<T> Diagnostic for std::sync::Arc<T>
where
    T: Diagnostic,
{
    fn rule(&self) -> &str {
        (**self).rule()
    }

    fn message(&self) -> std::borrow::Cow<str> {
        (**self).message()
    }

    fn file(&self) -> File {
        (**self).file()
    }

    fn range(&self) -> Option<TextRange> {
        (**self).range()
    }

    fn severity(&self) -> Severity {
        (**self).severity()
    }
}

impl Diagnostic for Box<dyn Diagnostic> {
    fn rule(&self) -> &str {
        (**self).rule()
    }

    fn message(&self) -> Cow<str> {
        (**self).message()
    }

    fn file(&self) -> File {
        (**self).file()
    }

    fn range(&self) -> Option<TextRange> {
        (**self).range()
    }

    fn severity(&self) -> Severity {
        (**self).severity()
    }
}

#[derive(Debug)]
pub struct ParseDiagnostic {
    file: File,
    error: ParseError,
}

impl ParseDiagnostic {
    pub fn new(file: File, error: ParseError) -> Self {
        Self { file, error }
    }
}

impl Diagnostic for ParseDiagnostic {
    fn rule(&self) -> &str {
        "invalid-syntax"
    }

    fn message(&self) -> Cow<str> {
        self.error.error.to_string().into()
    }

    fn file(&self) -> File {
        self.file
    }

    fn range(&self) -> Option<TextRange> {
        Some(self.error.location)
    }

    fn severity(&self) -> Severity {
        Severity::Error
    }
}
