use core::result::Result;
use std::path::{Component, PathBuf};
use std::sync::mpsc::SyncSender;

use comrak::Arena;
use globset::GlobSet;
use ignore::{DirEntry, Error, ParallelVisitor, ParallelVisitorBuilder, WalkState};
use miette::IntoDiagnostic as _;

use super::Linter;
use crate::{Document, Violation, config::Config};

pub struct MarkdownLintVisitor {
    linter: Linter,
    exclusion: GlobSet,
    tx: SyncSender<Vec<Violation>>,
}

impl MarkdownLintVisitor {
    #[inline]
    #[must_use]
    pub const fn new(linter: Linter, exclusion: GlobSet, tx: SyncSender<Vec<Violation>>) -> Self {
        Self {
            linter,
            exclusion,
            tx,
        }
    }

    fn visit_inner(&self, either_entry: Result<DirEntry, Error>) -> miette::Result<()> {
        let entry = either_entry.into_diagnostic()?;
        let path = entry.path();
        if path.is_file() && path.extension() == Some("md".as_ref()) {
            // Strip a leading "./" so that exclude patterns match regardless of
            // whether the walked path carries one (depends on how the target
            // argument was spelled on the command line, see issue #168). Keep
            // this in sync with the pattern normalization in
            // Lint::exclude_set (src/config/lint.rs) or the two sides stop
            // agreeing on what a match is.
            let normalized_path: PathBuf = path
                .components()
                .skip_while(|component| matches!(component, Component::CurDir))
                .collect();

            if !self.exclusion.is_match(&normalized_path) {
                let arena = Arena::new();
                let doc = Document::open(&arena, path)?;
                let violations = self.linter.check(&doc)?;
                if !violations.is_empty() {
                    self.tx.send(violations).into_diagnostic()?;
                }
            }
        }

        Ok(())
    }
}

impl ParallelVisitor for MarkdownLintVisitor {
    #[inline]
    fn visit(&mut self, either_entry: Result<DirEntry, Error>) -> WalkState {
        if let Err(err) = self.visit_inner(either_entry) {
            // TODO: Handle errors
            eprintln!("{err}");
        }
        WalkState::Continue
    }
}

pub struct MarkdownLintVisitorFactory {
    config: Config,
    exclusion: GlobSet,
    tx: SyncSender<Vec<Violation>>,
}

impl MarkdownLintVisitorFactory {
    #[inline]
    pub fn new(config: Config, tx: SyncSender<Vec<Violation>>) -> miette::Result<Self> {
        let exclusion = config.lint.exclude_set()?;
        Ok(Self {
            config,
            exclusion,
            tx,
        })
    }
}

impl<'s> ParallelVisitorBuilder<'s> for MarkdownLintVisitorFactory {
    #[inline]
    fn build(&mut self) -> Box<dyn ParallelVisitor + 's> {
        let linter = Linter::from(&self.config);
        Box::new(MarkdownLintVisitor::new(
            linter,
            self.exclusion.clone(),
            self.tx.clone(),
        ))
    }
}

#[cfg(test)]
mod tests {
    use std::sync::mpsc;

    use ignore::Walk;

    use super::*;

    #[test]
    fn markdown_lint_visitor_visit_inner() -> miette::Result<()> {
        let (tx, rx) = mpsc::sync_channel::<Vec<Violation>>(0);
        let linter = Linter::new(vec![]);
        let exclusion = GlobSet::empty();
        let visitor = MarkdownLintVisitor::new(linter, exclusion, tx);

        for entry in Walk::new(".") {
            visitor.visit_inner(entry)?;
        }

        drop(visitor);
        assert!(rx.recv().is_err()); // Because rx has not received any messages
        Ok(())
    }

    #[test]
    fn markdown_lint_visitor_factory_build() -> miette::Result<()> {
        let mut config = Config::default();
        config.lint.rules = vec![];

        let (tx, rx) = mpsc::sync_channel::<Vec<Violation>>(0);
        let mut factory = MarkdownLintVisitorFactory::new(config, tx)?;
        let mut visitor = factory.build();

        for entry in Walk::new(".") {
            visitor.visit(entry);
        }

        drop(visitor);
        drop(factory);
        assert!(rx.recv().is_err()); // Because rx has not received any messages
        Ok(())
    }
}
