repokit 5.0.0

A knowledgebase for your repository - wrapped in a CLI
use std::{
    fs::File,
    io::{BufRead, BufReader},
    path::{Path, PathBuf},
    sync::{Arc, LazyLock, Mutex},
};

use ignore::{DirEntry, Error, ParallelVisitor, ParallelVisitorBuilder, WalkState};
use regex::Regex;
use tokio::task::JoinSet;

use crate::{internal_filesystem::file_builder::FileBuilder, logger::logger::Logger};

pub struct TSCommandVisitor {
    root: PathBuf,
    paths: Arc<Mutex<Vec<String>>>,
    root_replacer: String,
}

static REPOKIT_IMPORT_MATCHER: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r#"(require\(|from[\s*]?)['"]@repokit/core["'][\)]?[;]?$"#).unwrap()
});

impl ParallelVisitor for TSCommandVisitor {
    fn visit(&mut self, entry: Result<DirEntry, Error>) -> WalkState {
        let root_replacer = format!("{}/", self.root_replacer);
        if let Ok(file_entry) = entry
            && file_entry.file_type().is_some_and(|ft| ft.is_file())
            && file_entry.file_name().to_string_lossy().ends_with(".ts")
            && let Some(matched_path) =
                TSCommandVisitor::on_file(&self.root.join(file_entry.path()))
        {
            let mut vector = self.paths.lock().unwrap();
            vector.push(matched_path.to_string_lossy().replace(&root_replacer, ""));
        }

        WalkState::Continue
    }
}

impl TSCommandVisitor {
    #[tokio::main]
    pub async fn traverse_list(root: &PathBuf, path_list: Vec<String>) -> Vec<String> {
        let mut handles = JoinSet::new();
        let root_replacer = format!("{}/", root.to_string_lossy());
        for path in path_list {
            let root_clone = root.to_owned();
            handles.spawn(async move {
                if let Some(result) = TSCommandVisitor::on_file(&root_clone.join(Path::new(&path)))
                {
                    return Some(result.to_owned());
                }
                None
            });
        }
        handles
            .join_all()
            .await
            .iter()
            .filter_map(|result| {
                result
                    .as_ref()
                    .map(|path| path.to_string_lossy().replace(&root_replacer, ""))
            })
            .collect()
    }

    pub fn on_file(path: &Path) -> Option<&Path> {
        let mut open_comment = false;
        let file: File = FileBuilder::open(path, |_| Logger::open_file_error());
        let reader: BufReader<File> = BufReader::new(file);
        for line_result in reader.lines() {
            let unwrapped = line_result.unwrap();
            let line = unwrapped.trim();
            if !open_comment && line.starts_with("/*") {
                open_comment = true;
                continue;
            }
            if open_comment {
                if line.ends_with("*/") {
                    open_comment = false;
                }
                continue;
            }
            if REPOKIT_IMPORT_MATCHER.is_match(line) {
                return Some(path);
            }
            if !line.is_empty()
                && !line.starts_with("import ")
                && !line.contains("require(")
                && !(line.starts_with("//") || line.starts_with("/*"))
            {
                break;
            }
        }
        None
    }
}

pub struct TSCommandVisitorBuilder<'a> {
    pub root: &'a PathBuf,
    pub paths: &'a Arc<Mutex<Vec<String>>>,
}

impl<'a> TSCommandVisitorBuilder<'a> {
    pub fn new(
        root: &'a PathBuf,
        paths: &'a Arc<Mutex<Vec<String>>>,
    ) -> TSCommandVisitorBuilder<'a> {
        TSCommandVisitorBuilder { paths, root }
    }
}

impl<'s> ParallelVisitorBuilder<'s> for TSCommandVisitorBuilder<'s> {
    fn build(&mut self) -> Box<dyn ParallelVisitor + 's> {
        Box::new(TSCommandVisitor {
            paths: self.paths.clone(),
            root: self.root.to_owned(),
            root_replacer: self.root.to_string_lossy().to_string(),
        })
    }
}