use crate::config::{Config, ConfigOption};
use libloading::{Library, Symbol};
use std::path::{Path, PathBuf};
use sv_parser::{unwrap_locate, Locate, NodeEvent, SyntaxTree};

#[derive(Clone, Copy)]
pub enum RuleResult {
    Pass,
    Fail,
    FailAt(usize, usize),
    FailLocate(Locate),
}

pub trait Rule: Sync + Send {
    fn check(&mut self, syntax_tree: &SyntaxTree, event: &NodeEvent) -> RuleResult;
    fn name(&self) -> String;
    fn hint(&self) -> String;
    fn reason(&self) -> String;
}

pub struct Linter {
    option: ConfigOption,
    rules: Vec<Box<dyn Rule>>,
    plugins: Vec<Library>,
}

#[derive(Debug)]
pub struct LintFailed {
    pub path: PathBuf,
    pub beg: usize,
    pub len: usize,
    pub name: String,
    pub hint: String,
    pub reason: String,
}

impl Linter {
    pub fn new(config: Config) -> Linter {
        let rules = config.gen_rules();
        Linter {
            option: config.option,
            rules,
            plugins: Vec::new(),
        }
    }

    pub fn load(&mut self, path: &Path) {
        let lib = Library::new(path);
        if let Ok(lib) = lib {
            self.plugins.push(lib);
            let lib = self.plugins.last().unwrap();

            let get_plugin: Result<Symbol<extern "C" fn() -> *mut dyn Rule>, _> =
                unsafe { lib.get(b"get_plugin") };
            if let Ok(get_plugin) = get_plugin {
                let plugin = unsafe { Box::from_raw(get_plugin()) };
                self.rules.push(plugin);
            }
        }
    }

    pub fn check(&mut self, syntax_tree: &SyntaxTree, event: &NodeEvent) -> Vec<LintFailed> {
        let node = match event {
            NodeEvent::Enter(x) => x,
            NodeEvent::Leave(x) => x,
        };
        let locate = if let Some(x) = unwrap_locate!(node.clone()) {
            x
        } else {
            return vec![];
        };

        let mut ret = Vec::new();
        'outer: for rule in &mut self.rules {
            match rule.check(syntax_tree, event) {
                RuleResult::Fail => {
                    if let Some((path, beg)) = syntax_tree.get_origin(&locate) {
                        for exclude in &self.option.exclude_paths {
                            if exclude.is_match(&path.to_string_lossy()) {
                                continue 'outer;
                            }
                        }
                        let result = LintFailed {
                            path: path.clone(),
                            beg,
                            len: locate.len,
                            name: rule.name(),
                            hint: rule.hint(),
                            reason: rule.reason(),
                        };
                        ret.push(result);
                    }
                }
                RuleResult::FailAt(offset, len) => {
                    if let Some((path, beg)) = syntax_tree.get_origin(&locate) {
                        for exclude in &self.option.exclude_paths {
                            if exclude.is_match(&path.to_string_lossy()) {
                                continue 'outer;
                            }
                        }
                        let result = LintFailed {
                            path: path.clone(),
                            beg: beg + offset,
                            len,
                            name: rule.name(),
                            hint: rule.hint(),
                            reason: rule.reason(),
                        };
                        ret.push(result);
                    }
                }
                RuleResult::FailLocate(x) => {
                    if let Some((path, beg)) = syntax_tree.get_origin(&x) {
                        for exclude in &self.option.exclude_paths {
                            if exclude.is_match(&path.to_string_lossy()) {
                                continue 'outer;
                            }
                        }
                        let result = LintFailed {
                            path: path.clone(),
                            beg,
                            len: x.len,
                            name: rule.name(),
                            hint: rule.hint(),
                            reason: rule.reason(),
                        };
                        ret.push(result);
                    }
                }
                _ => (),
            }
        }
        ret
    }
}
