#![feature(rustc_private)]

extern crate rustc_driver;
extern crate rustc_log;
extern crate rustc_session;

extern crate rustc_errors;
use std::env;
use std::error::Error;
use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::str::FromStr;

use mdbook::book::{BookItem, Chapter, parse_summary};
use mdbook::{Config, MDBook};
use rustc_errors::codes::DIAGNOSTICS;

enum OutputFormat {
    HTML,
    Markdown,
    Unknown(String),
}

impl OutputFormat {
    fn from(format: &str) -> OutputFormat {
        match &*format.to_lowercase() {
            "html" => OutputFormat::HTML,
            "markdown" => OutputFormat::Markdown,
            s => OutputFormat::Unknown(s.to_owned()),
        }
    }
}

/// Output an HTML page for the errors in `err_map` to `output_path`.
fn render_markdown(output_path: &Path) -> Result<(), Box<dyn Error>> {
    let mut output_file = File::create(output_path)?;

    write!(output_file, "# Rust Compiler Error Index\n")?;

    for (err_code, description) in DIAGNOSTICS.iter() {
        write!(output_file, "## {}\n{}\n", err_code, description)?
    }

    Ok(())
}

// By default, mdbook doesn't consider code blocks as Rust ones contrary to rustdoc so we have
// to manually add `rust` attribute whenever needed.
fn add_rust_attribute_on_codeblock(explanation: &str) -> String {
    // Very hacky way to add the rust attribute on all code blocks.
    let mut skip = true;
    explanation.split("\n```").fold(String::new(), |mut acc, part| {
        if !acc.is_empty() {
            acc.push_str("\n```");
        }
        if !skip {
            if let Some(attrs) = part.split('\n').next() {
                if !attrs.contains("rust")
                    && (attrs.is_empty()
                        || attrs.contains("compile_fail")
                        || attrs.contains("ignore")
                        || attrs.contains("edition"))
                {
                    if !attrs.is_empty() {
                        acc.push_str("rust,");
                    } else {
                        acc.push_str("rust");
                    }
                }
            }
        }
        skip = !skip;
        acc.push_str(part);
        acc
    })
}

fn render_html(output_path: &Path) -> Result<(), Box<dyn Error>> {
    let mut introduction = format!(
        "# Rust error codes index

This page lists all the error codes emitted by the Rust compiler.

"
    );

    let err_codes = DIAGNOSTICS;
    let mut chapters = Vec::with_capacity(err_codes.len());

    for (err_code, explanation) in err_codes.iter() {
        introduction.push_str(&format!(" * [{0}](./{0}.html)\n", err_code));

        let content = add_rust_attribute_on_codeblock(explanation);
        chapters.push(BookItem::Chapter(Chapter {
            name: err_code.to_string(),
            content: format!("# Error code {}\n\n{}\n", err_code, content),
            number: None,
            sub_items: Vec::new(),
            // We generate it into the `error_codes` folder.
            path: Some(PathBuf::from(&format!("{}.html", err_code))),
            source_path: None,
            parent_names: Vec::new(),
        }));
    }

    let mut config = Config::from_str(include_str!("book_config.toml"))?;
    config.build.build_dir = output_path.join("error_codes").to_path_buf();
    let mut book = MDBook::load_with_config_and_summary(
        env!("CARGO_MANIFEST_DIR"),
        config,
        parse_summary("")?,
    )?;
    let chapter = Chapter {
        name: "Rust error codes index".to_owned(),
        content: introduction,
        number: None,
        sub_items: chapters,
        // Very important: this file is named as `error-index.html` and not `index.html`!
        path: Some(PathBuf::from("error-index.html")),
        source_path: None,
        parent_names: Vec::new(),
    };
    book.book.sections.push(BookItem::Chapter(chapter));
    book.build()?;

    // The error-index used to be generated manually (without mdbook), and the
    // index was located at the top level. Now that it is generated with
    // mdbook, error-index.html has moved to error_codes/error-index.html.
    // This adds a redirect so that old links go to the new location.
    //
    // We can't put this content into another file, otherwise `mdbook` will also put it into the
    // output directory, making a duplicate.
    fs::write(
        output_path.join("error-index.html"),
        r#"<!DOCTYPE html>
<html>
    <head>
        <title>Rust error codes index - Error codes index</title>
        <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
        <meta name="description" content="Book listing all Rust error codes">
        <script src="error_codes/redirect.js"></script>
    </head>
    <body>
        <div>If you are not automatically redirected to the error code index, please <a id="index-link" href="./error_codes/error-index.html">here</a>.
    </body>
</html>"#,
    )?;

    Ok(())
}

fn main_with_result(format: OutputFormat, dst: &Path) -> Result<(), Box<dyn Error>> {
    match format {
        OutputFormat::Unknown(s) => panic!("Unknown output format: {}", s),
        OutputFormat::HTML => render_html(dst),
        OutputFormat::Markdown => render_markdown(dst),
    }
}

fn parse_args() -> (OutputFormat, PathBuf) {
    let mut args = env::args().skip(1);
    let format = args.next();
    let dst = args.next();
    let format = format.map(|a| OutputFormat::from(&a)).unwrap_or(OutputFormat::from("html"));
    let dst = dst.map(PathBuf::from).unwrap_or_else(|| match format {
        OutputFormat::HTML => PathBuf::from("doc"),
        OutputFormat::Markdown => PathBuf::from("doc/error-index.md"),
        OutputFormat::Unknown(..) => PathBuf::from("<nul>"),
    });
    (format, dst)
}

fn main() {
    let handler =
        rustc_session::EarlyDiagCtxt::new(rustc_session::config::ErrorOutputType::default());
    rustc_driver::init_logger(&handler, rustc_log::LoggerConfig::from_env("RUST_LOG"));
    let (format, dst) = parse_args();
    let result = main_with_result(format, &dst);
    if let Err(e) = result {
        panic!("{:?}", e);
    }
}
