mod block;
mod highlighter;
mod index;
mod inline;
mod tests;
mod util;

use std::collections::HashMap;

use pretty::{Arena, DocBuilder};

use crate::ast::{Document, Inline, LinkDefinition};

pub trait UrlRewriter {
    /// Rewrite a given URL to someplace new.
    ///
    /// `target` is only used for Wikilinks currently.
    fn rewrite(&self, path: &str, target: Option<&str>) -> String;
}

pub struct Config<'a> {
    pub(crate) width: usize,
    pub(crate) link_rewriter: Option<Box<dyn UrlRewriter + 'a>>,
    pub(crate) wikilink_rewriter: Option<Box<dyn UrlRewriter + 'a>>,
}

impl Default for Config<'_> {
    fn default() -> Self {
        Self {
            width: 80,
            link_rewriter: None,
            wikilink_rewriter: None,
        }
    }
}

impl<'a> Config<'a> {
    #[must_use]
    pub fn with_width(self, width: usize) -> Self {
        Self { width, ..self }
    }

    #[must_use]
    pub fn with_link_rewriter<R: UrlRewriter + 'a>(self, url_rewriter: R) -> Self {
        Self {
            link_rewriter: Some(Box::new(url_rewriter)),
            ..self
        }
    }

    #[must_use]
    pub fn with_wikilink_rewriter<R: UrlRewriter + 'a>(self, url_rewriter: R) -> Self {
        Self {
            wikilink_rewriter: Some(Box::new(url_rewriter)),
            ..self
        }
    }

    pub(crate) fn rewrite_link(&self, path: &str, target: Option<&str>) -> String {
        if let Some(rewriter) = &self.link_rewriter {
            return rewriter.rewrite(path, target);
        }

        path.to_owned()
    }

    pub(crate) fn rewrite_wikilink(&self, path: &str, target: Option<&str>) -> String {
        if let Some(rewriter) = &self.wikilink_rewriter {
            return rewriter.rewrite(path, target);
        }

        path.to_owned()
    }
}

pub(crate) struct Context {
    // Mapping of footnote labels to their indices in the footnote list.
    footnote_index: HashMap<String, usize>,
    // Mapping of link labels to their definitions.
    link_definitions: HashMap<Vec<Inline>, LinkDefinition>,
}

impl Context {
    pub(crate) fn new(ast: &Document) -> Self {
        let (footnote_index, link_definitions) = index::get_indicies(ast);
        Self {
            footnote_index,
            link_definitions,
        }
    }

    pub(crate) fn get_footnote_index(&self, label: &str) -> Option<&usize> {
        self.footnote_index.get(label)
    }

    pub(crate) fn get_link_definition(&self, label: &Vec<Inline>) -> Option<&LinkDefinition> {
        self.link_definitions.get(label)
    }
}

/// Render the given Markdown AST to HTML.
#[must_use]
pub fn render_html(ast: &Document, config: &Config<'_>) -> String {
    let mut buf = Vec::new();

    {
        let width = config.width;

        let context = Context::new(ast);
        let arena = Arena::new();
        ast.to_doc(config, &context, &arena)
            .render(width, &mut buf)
            .unwrap();
    }

    String::from_utf8(buf).unwrap()
}

trait ToDoc<'a> {
    fn to_doc(
        &self,
        config: &'a Config<'a>,
        context: &'a Context,
        arena: &'a Arena<'a>,
    ) -> DocBuilder<'a, Arena<'a>, ()>;
}

impl<'a> ToDoc<'a> for Document {
    fn to_doc(
        &self,
        config: &'a Config<'a>,
        context: &'a Context,
        arena: &'a Arena<'a>,
    ) -> DocBuilder<'a, Arena<'a>, ()> {
        self.blocks.to_doc(config, context, arena)
    }
}
