use anyhow::Result;
use schemars::JsonSchema;
use serde::Serialize;
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
use crate::{executor::SourceRange, lsp::IntoDiagnostic, walk::Node};
pub trait Rule<'a> {
    fn check(&self, node: Node<'a>) -> Result<Vec<Discovered>>;
}
impl<'a, FnT> Rule<'a> for FnT
where
    FnT: Fn(Node<'a>) -> Result<Vec<Discovered>>,
{
    fn check(&self, n: Node<'a>) -> Result<Vec<Discovered>> {
        self(n)
    }
}
#[derive(Clone, Debug, ts_rs::TS, Serialize, JsonSchema)]
#[ts(export)]
#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
#[serde(rename_all = "camelCase")]
pub struct Discovered {
    pub finding: Finding,
    pub description: String,
    pub pos: SourceRange,
    pub overridden: bool,
}
#[cfg(feature = "pyo3")]
#[pyo3::pymethods]
impl Discovered {
    #[getter]
    pub fn finding(&self) -> Finding {
        self.finding.clone()
    }
    #[getter]
    pub fn description(&self) -> String {
        self.description.clone()
    }
    #[getter]
    pub fn pos(&self) -> SourceRange {
        self.pos
    }
    #[getter]
    pub fn overridden(&self) -> bool {
        self.overridden
    }
}
impl IntoDiagnostic for Discovered {
    fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
        (&self).to_lsp_diagnostic(code)
    }
    fn severity(&self) -> DiagnosticSeverity {
        (&self).severity()
    }
}
impl IntoDiagnostic for &Discovered {
    fn to_lsp_diagnostic(&self, code: &str) -> Diagnostic {
        let message = self.finding.title.to_owned();
        let source_range = self.pos;
        Diagnostic {
            range: source_range.to_lsp_range(code),
            severity: Some(self.severity()),
            code: None,
            code_description: None,
            source: Some("lint".to_string()),
            message,
            related_information: None,
            tags: None,
            data: None,
        }
    }
    fn severity(&self) -> DiagnosticSeverity {
        DiagnosticSeverity::INFORMATION
    }
}
#[derive(Clone, Debug, PartialEq, ts_rs::TS, Serialize, JsonSchema)]
#[ts(export)]
#[cfg_attr(feature = "pyo3", pyo3::pyclass)]
#[serde(rename_all = "camelCase")]
pub struct Finding {
    pub code: &'static str,
    pub title: &'static str,
    pub description: &'static str,
    pub experimental: bool,
}
impl Finding {
    pub fn at(&self, description: String, pos: SourceRange) -> Discovered {
        Discovered {
            description,
            finding: self.clone(),
            pos,
            overridden: false,
        }
    }
}
#[cfg(feature = "pyo3")]
#[pyo3::pymethods]
impl Finding {
    #[getter]
    pub fn code(&self) -> &'static str {
        self.code
    }
    #[getter]
    pub fn title(&self) -> &'static str {
        self.title
    }
    #[getter]
    pub fn description(&self) -> &'static str {
        self.description
    }
    #[getter]
    pub fn experimental(&self) -> bool {
        self.experimental
    }
}
macro_rules! def_finding {
    ( $code:ident, $title:expr, $description:expr ) => {
        pub const $code: Finding = $crate::lint::rule::finding!($code, $title, $description);
    };
}
pub(crate) use def_finding;
macro_rules! finding {
    ( $code:ident, $title:expr, $description:expr ) => {
        $crate::lint::rule::Finding {
            code: stringify!($code),
            title: $title,
            description: $description,
            experimental: false,
        }
    };
}
pub(crate) use finding;
#[cfg(test)]
pub(crate) use test::{assert_finding, assert_no_finding, test_finding, test_no_finding};
#[cfg(test)]
mod test {
    macro_rules! assert_no_finding {
        ( $check:expr, $finding:expr, $kcl:expr ) => {
            let tokens = $crate::token::lexer($kcl).unwrap();
            let parser = $crate::parser::Parser::new(tokens);
            let prog = parser.ast().unwrap();
            for discovered_finding in prog.lint($check).unwrap() {
                if discovered_finding.finding == $finding {
                    assert!(false, "Finding {:?} was emitted", $finding.code);
                }
            }
        };
    }
    macro_rules! assert_finding {
        ( $check:expr, $finding:expr, $kcl:expr ) => {
            let tokens = $crate::token::lexer($kcl).unwrap();
            let parser = $crate::parser::Parser::new(tokens);
            let prog = parser.ast().unwrap();
            for discovered_finding in prog.lint($check).unwrap() {
                if discovered_finding.finding == $finding {
                    return;
                }
            }
            assert!(false, "Finding {:?} was not emitted", $finding.code);
        };
    }
    macro_rules! test_finding {
        ( $name:ident, $check:expr, $finding:expr, $kcl:expr ) => {
            #[test]
            fn $name() {
                $crate::lint::rule::assert_finding!($check, $finding, $kcl);
            }
        };
    }
    macro_rules! test_no_finding {
        ( $name:ident, $check:expr, $finding:expr, $kcl:expr ) => {
            #[test]
            fn $name() {
                $crate::lint::rule::assert_no_finding!($check, $finding, $kcl);
            }
        };
    }
    pub(crate) use assert_finding;
    pub(crate) use assert_no_finding;
    pub(crate) use test_finding;
    pub(crate) use test_no_finding;
}