use comrak::{
    self,
    nodes::{AstNode, NodeCodeBlock, NodeHeading, NodeHtmlBlock, NodeLink, NodeValue},
    Arena,
};
use docs::{
    page::{Page, COMRAK_OPTIONS},
    views,
};
use std::{
    env,
    error::Error,
    fs::{self, File},
    io::BufReader,
    path::Path,
    str::{self, Utf8Error},
    string::FromUtf8Error,
};
use syntect::{
    highlighting::{Color, ThemeSet},
    html::highlighted_html_for_string,
    parsing::SyntaxSet,
};

fn main() -> Result<(), Box<dyn Error>> {
    let args = env::args().collect::<Vec<_>>();
    if args.len() != 7 {
        return Err("invalid arguments".into());
    }
    build_page(&args[1], &args[2], &args[3], &args[4], &args[5], &args[6])
}

fn build_page(
    output_path: &str,
    slug: &str,
    input_path: &str,
    nav_path: &str,
    version: &str,
    hash: &str,
) -> Result<(), Box<dyn Error>> {
    let nav: Vec<(String, Option<String>)> = {
        let file = File::open(nav_path)?;
        let reader = BufReader::new(file);
        serde_json::from_reader(reader)?
    };

    let arena = Arena::new();

    let nav = nav
        .iter()
        .filter_map(|(slug, title)| {
            title.as_ref().map(|title| {
                let title = comrak::parse_document(&arena, title, &COMRAK_OPTIONS);
                (slug.as_str(), title)
            })
        })
        .collect::<Vec<_>>();

    let page = Page::load(&arena, input_path)?;
    postprocess(page.content)?;

    let markup = views::main(slug, page, &nav, version, hash);

    fs::create_dir_all(Path::new(output_path).parent().unwrap())?;
    fs::write(output_path, markup.into_string())?;

    Ok(())
}

fn postprocess<'a>(content: &'a AstNode<'a>) -> Result<(), Box<dyn Error>> {
    lower_headings(content);
    rewrite_md_links(content)?;
    strip_hidden_code(content)?;
    highlight_code(content)?;
    Ok(())
}

fn lower_headings<'a>(root: &'a AstNode<'a>) {
    for node in root.descendants() {
        let mut data = node.data.borrow_mut();
        if let NodeValue::Heading(NodeHeading { level, .. }) = &mut data.value {
            *level += 1;
        }
    }
}

fn rewrite_md_links<'a>(root: &'a AstNode<'a>) -> Result<(), FromUtf8Error> {
    for node in root.descendants() {
        let mut data = node.data.borrow_mut();
        if let NodeValue::Link(NodeLink { url, .. }) = &mut data.value {
            let mut url_string = String::from_utf8(std::mem::take(url))?;
            if url_string.ends_with(".md") {
                url_string.truncate(url_string.len() - ".md".len());
                url_string.push_str(".html");
            }
            *url = url_string.into_bytes();
        }
    }
    Ok(())
}

fn strip_hidden_code<'a>(root: &'a AstNode<'a>) -> Result<(), Box<dyn Error>> {
    for node in root.descendants() {
        let mut data = node.data.borrow_mut();
        if let NodeValue::CodeBlock(NodeCodeBlock { info, literal, .. }) = &mut data.value {
            let info = parse_code_block_info(info)?;
            if !info.contains(&"rust") {
                continue;
            }
            *literal = strip_hidden_code_inner(str::from_utf8(literal)?).into_bytes();
        }
    }
    Ok(())
}

fn strip_hidden_code_inner(literal: &str) -> String {
    let lines = literal
        .split('\n')
        .filter(|line| {
            let line = line.trim();
            line != "#" && !line.starts_with("# ")
        })
        .collect::<Vec<_>>();
    lines.join("\n")
}

fn highlight_code<'a>(root: &'a AstNode<'a>) -> Result<(), Box<dyn Error>> {
    let ss = SyntaxSet::load_defaults_newlines();
    let ts = ThemeSet::load_defaults();
    let mut theme = ts.themes["InspiredGitHub"].clone();
    theme.settings.background = Some(Color {
        r: 0xff,
        g: 0xee,
        b: 0xff,
        a: 0xff,
    });
    for node in root.descendants() {
        let mut data = node.data.borrow_mut();
        if let NodeValue::CodeBlock(NodeCodeBlock { info, literal, .. }) = &mut data.value {
            let info = parse_code_block_info(info)?;
            let syntax = info
                .into_iter()
                .filter_map(|token| ss.find_syntax_by_token(token))
                .next()
                .unwrap_or_else(|| ss.find_syntax_plain_text());
            let mut literal = String::from_utf8(std::mem::take(literal))?;
            if !literal.ends_with('\n') {
                // Syntect expects a trailing newline
                literal.push('\n');
            }
            let html = highlighted_html_for_string(&literal, &ss, syntax, &theme);
            let mut html_block = NodeHtmlBlock::default();
            html_block.literal = html.into_bytes();
            data.value = NodeValue::HtmlBlock(html_block);
        }
    }
    Ok(())
}

fn parse_code_block_info(info: &[u8]) -> Result<Vec<&str>, Utf8Error> {
    str::from_utf8(info).map(|info| info.split(',').map(str::trim).collect())
}