pub mod boolean_calculator;

use anyhow::{Result, anyhow};
use clap::{Arg, ArgMatches, Command};
use fancy_regex::Regex;
use mdbook::BookItem;
use mdbook::book::Book;
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
use parking_lot::RwLock;
use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::io::{BufReader, stdin, stdout};
use std::path::PathBuf;
use std::{process, str};

pub fn handle_table_generation_err(err: Result<String>) -> String {
    match err {
        Ok(s) => s,
        Err(e) => format!("<div class=\"warning\"> {} </div>", e),
    }
}

pub fn make_app() -> Command {
    Command::new("mdbook-glossary")
        .about("Reads YAML glossary files and inserts tag-specific glossaries.")
        .subcommand(
            Command::new("supports")
                .arg(Arg::new("renderer").required(true))
                .about("Check whether a renderer is supported by this preprocessor"),
        )
}

fn main() {
    let matches = make_app().get_matches();

    let preprocessor = Glossary::new();

    if let Some(sub_args) = matches.subcommand_matches("supports") {
        handle_supports(&preprocessor, sub_args);
    } else if let Err(e) = handle_preprocessing(&preprocessor) {
        eprintln!("{e:?}");
        process::exit(1);
    }
}

fn handle_preprocessing(pre: &impl Preprocessor) -> Result<()> {
    let (ctx, book) = CmdPreprocessor::parse_input(stdin())?;

    let book_version = Version::parse(&ctx.mdbook_version)?;
    let version_req = VersionReq::parse(mdbook::MDBOOK_VERSION)?;

    if !version_req.matches(&book_version) {
        eprintln!(
            "Warning: The {} plugin was built against version {} of mdBook, \
             but we're being called from version {}",
            pre.name(),
            mdbook::MDBOOK_VERSION,
            ctx.mdbook_version
        );
    }

    let processed_book = pre.run(&ctx, book)?;
    serde_json::to_writer(stdout(), &processed_book)?;

    Ok(())
}

fn handle_supports(pre: &impl Preprocessor, sub_args: &ArgMatches) -> ! {
    let renderer = sub_args
        .get_one::<String>("renderer")
        .expect("Required argument");
    let supported = pre.supports_renderer(renderer);

    // Signal whether the renderer is supported by exiting with 1 or 0.
    if supported {
        process::exit(0);
    } else {
        process::exit(1);
    }
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Definition {
    term: String,
    definition: String,
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DefinitionId(usize);

#[derive(Debug)]
pub struct Glossary {
    definitions: RwLock<Vec<Definition>>,
    tags: RwLock<HashMap<String, HashSet<DefinitionId>>>,
}

impl Default for Glossary {
    fn default() -> Self {
        Self::new()
    }
}

impl Glossary {
    pub fn new() -> Glossary {
        Glossary {
            definitions: Vec::new().into(),
            tags: HashMap::new().into(),
        }
    }

    pub fn add_definition(&self, dfn: Definition, tags: &[String]) {
        let insert_idx = self.definitions.read().len();
        self.definitions.write().push(dfn);

        let dfn_id = DefinitionId(insert_idx);

        for tag in tags {
            let tag = tag.to_owned();
            self.tags.write().entry(tag).or_default().insert(dfn_id);
        }
    }

    pub fn generate_dictionary(&self, operation: String) -> Result<String> {
        static REGEX: std::sync::LazyLock<Regex> =
            std::sync::LazyLock::new(|| Regex::new(r"\w+").unwrap());

        let mut def_ids = Vec::<DefinitionId>::new();
        let def_len = self.definitions.read().len();
        for idx in 0..def_len {
            let def_id = DefinitionId(idx);

            let mut computation_string = operation.clone();
            let mut offset = 0usize;
            while let Some(mtch) = REGEX.find_from_pos(&computation_string, offset)? {
                let tags = self.tags.read();
                let val = match mtch.as_str() {
                    "1" => "1",
                    "0" => "0",
                    mtch => tags
                        .get(mtch)
                        .map(|s| if s.contains(&def_id) { "1" } else { "0" })
                        .ok_or(anyhow!("Could not find tag: \"{}\"", mtch))?,
                };
                let range = mtch.range();
                offset = mtch.start() + 1;

                computation_string.replace_range(range, val);
            }

            if boolean_calculator::evaluate(&computation_string)? {
                def_ids.push(def_id);
            }
        }

        def_ids.sort();
        def_ids.dedup();

        let mut table = String::new();
        table.push_str("| Term | Definition |\n| ---- | ---------- |\n");

        let definitions = self.definitions.read();
        for DefinitionId(id) in def_ids {
            let dfn = &definitions[id];
            table.push_str(&format!("| {} | {} |\n", dfn.term, dfn.definition));
        }

        Ok(table)
    }

    pub fn replace(&self, content: &mut String) -> Result<()> {
        static REGEX: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
            Regex::new(r"\{\{\s*#glossary\((?<tags>.*?)\)\s*\}\}").unwrap()
        });

        while REGEX.is_match(content)? {
            let capture = REGEX.captures(content)?.unwrap();
            let span = capture.get(0).unwrap().range();
            let tags = capture["tags"].trim().to_string();
            if tags == "*" {
                let tags = self
                    .tags
                    .read()
                    .keys()
                    .map(String::as_str)
                    .collect::<Vec<_>>()
                    .join(" | ");

                content.replace_range(
                    span,
                    &handle_table_generation_err(self.generate_dictionary(tags)),
                );
            } else {
                content.replace_range(
                    span,
                    &handle_table_generation_err(self.generate_dictionary(tags)),
                );
            }
        }

        Ok(())
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct GlossaryConfig {
    paths: Vec<PathBuf>,
}

impl Preprocessor for Glossary {
    fn name(&self) -> &str {
        "glossary"
    }

    fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
        #[derive(Deserialize)]
        pub struct YmlDefinition {
            term: String,
            definition: String,
            tags: Vec<String>,
        }

        if let Some(glossary_cfg) = ctx.config.get_preprocessor(self.name()) {
            let glossary_cfg: GlossaryConfig = toml::from_str(&toml::to_string(glossary_cfg)?)?;

            for path in &glossary_cfg.paths {
                let file = BufReader::new(File::open(path)?);
                let definitions: Vec<YmlDefinition> = serde_yml::from_reader(file)?;
                for dfn in definitions {
                    let YmlDefinition {
                        term,
                        definition,
                        tags,
                    } = dfn;
                    self.add_definition(
                        Definition {
                            term,
                            definition: definition.replace("\n", "<br><br>"),
                        },
                        &tags,
                    );
                }
            }
        }

        let mut output = book.clone();

        output.for_each_mut(|m| {
            if let BookItem::Chapter(c) = m {
                self.replace(&mut c.content).unwrap();
            }
        });

        Ok(output)
    }
}

#[cfg(test)]
mod tests {
    use mdbook::preprocess::Preprocessor as _;

    use crate::Glossary;

    #[test]
    fn test_glossary() {
        let input_json = r##"[
                {
                    "root": "/path/to/book",
                    "config": {
                        "book": {
                            "authors": ["AUTHOR"],
                            "language": "en",
                            "multilingual": false,
                            "src": "src",
                            "title": "TITLE"
                        },
                        "preprocessor": {
                            "glossary": {
                                "paths": ["./tests/glossary.yml"]
                            }
                        }
                    },
                    "renderer": "html",
                    "mdbook_version": "0.4.56"
                },
                {
                    "sections": [
                        {
                            "Chapter": {
                                "name": "Chapter 1",
                                "content": "# Chapter 1\n {{ #glossary(*) }}\n {{ #glossary(!(Greek) & English) }}",
                                "number": [1],
                                "sub_items": [],
                                "path": "chapter_1.md",
                                "source_path": "chapter_1.md",
                                "parent_names": []
                            }
                        }
                    ],
                    "__non_exhaustive": null
                }
            ]"##;
        let input_json = input_json.as_bytes();

        let (ctx, book) = mdbook::preprocess::CmdPreprocessor::parse_input(input_json).unwrap();
        let result = Glossary::new().run(&ctx, book);
        assert!(result.is_ok());
        println!("{:#?}", result);
    }
}
