#![feature(rustc_private)]

extern crate rustc_driver;
extern crate rustc_span;

use std::cell::RefCell;
use std::collections::BTreeMap;
use std::env;
use std::error::Error;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;

use rustc_span::edition::DEFAULT_EDITION;

use rustdoc::html::markdown::{ErrorCodes, IdMap, Markdown, Playground};

pub struct ErrorMetadata {
    pub description: Option<String>,
}

/// Mapping from error codes to metadata that can be (de)serialized.
pub type ErrorMetadataMap = BTreeMap<String, ErrorMetadata>;

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

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

trait Formatter {
    fn header(&self, output: &mut dyn Write) -> Result<(), Box<dyn Error>>;
    fn title(&self, output: &mut dyn Write) -> Result<(), Box<dyn Error>>;
    fn error_code_block(
        &self,
        output: &mut dyn Write,
        info: &ErrorMetadata,
        err_code: &str,
    ) -> Result<(), Box<dyn Error>>;
    fn footer(&self, output: &mut dyn Write) -> Result<(), Box<dyn Error>>;
}

struct HTMLFormatter(RefCell<IdMap>, String);
struct MarkdownFormatter;

impl Formatter for HTMLFormatter {
    fn header(&self, output: &mut dyn Write) -> Result<(), Box<dyn Error>> {
        write!(
            output,
            r##"<!DOCTYPE html>
<html>
<head>
<title>Rust Compiler Error Index</title>
<meta charset="utf-8">
<!-- Include rust.css after light.css so its rules take priority. -->
<link rel="stylesheet" type="text/css" href="light{suffix}.css"/>
<link rel="stylesheet" type="text/css" href="rust.css"/>
<style>
.error-undescribed {{
    display: none;
}}
</style>
</head>
<body>
"##,
            suffix = self.1
        )?;
        Ok(())
    }

    fn title(&self, output: &mut dyn Write) -> Result<(), Box<dyn Error>> {
        write!(output, "<h1>Rust Compiler Error Index</h1>\n")?;
        Ok(())
    }

    fn error_code_block(
        &self,
        output: &mut dyn Write,
        info: &ErrorMetadata,
        err_code: &str,
    ) -> Result<(), Box<dyn Error>> {
        // Enclose each error in a div so they can be shown/hidden en masse.
        let desc_desc = match info.description {
            Some(_) => "error-described",
            None => "error-undescribed",
        };
        write!(output, "<div class=\"{}\">", desc_desc)?;

        // Error title (with self-link).
        write!(
            output,
            "<h2 id=\"{0}\" class=\"section-header\"><a href=\"#{0}\">{0}</a></h2>\n",
            err_code
        )?;

        // Description rendered as markdown.
        match info.description {
            Some(ref desc) => {
                let mut id_map = self.0.borrow_mut();
                let playground = Playground {
                    crate_name: None,
                    url: String::from("https://play.rust-lang.org/"),
                };
                write!(
                    output,
                    "{}",
                    Markdown(
                        desc,
                        &[],
                        &mut id_map,
                        ErrorCodes::Yes,
                        DEFAULT_EDITION,
                        &Some(playground)
                    )
                    .into_string()
                )?
            }
            None => write!(output, "<p>No description.</p>\n")?,
        }

        write!(output, "</div>\n")?;
        Ok(())
    }

    fn footer(&self, output: &mut dyn Write) -> Result<(), Box<dyn Error>> {
        write!(
            output,
            r##"<script>
function onEach(arr, func) {{
    if (arr && arr.length > 0 && func) {{
        for (var i = 0; i < arr.length; i++) {{
            func(arr[i]);
        }}
    }}
}}

function hasClass(elem, className) {{
    if (elem && className && elem.className) {{
        var elemClass = elem.className;
        var start = elemClass.indexOf(className);
        if (start === -1) {{
            return false;
        }} else if (elemClass.length === className.length) {{
            return true;
        }} else {{
            if (start > 0 && elemClass[start - 1] !== ' ') {{
                return false;
            }}
            var end = start + className.length;
            if (end < elemClass.length && elemClass[end] !== ' ') {{
                return false;
            }}
            return true;
        }}
        if (start > 0 && elemClass[start - 1] !== ' ') {{
            return false;
        }}
        var end = start + className.length;
        if (end < elemClass.length && elemClass[end] !== ' ') {{
            return false;
        }}
        return true;
    }}
    return false;
}}

onEach(document.getElementsByClassName('rust-example-rendered'), function(e) {{
    if (hasClass(e, 'compile_fail')) {{
        e.addEventListener("mouseover", function(event) {{
            e.previousElementSibling.childNodes[0].style.color = '#f00';
        }});
        e.addEventListener("mouseout", function(event) {{
            e.previousElementSibling.childNodes[0].style.color = '';
        }});
    }} else if (hasClass(e, 'ignore')) {{
        e.addEventListener("mouseover", function(event) {{
            e.previousElementSibling.childNodes[0].style.color = '#ff9200';
        }});
        e.addEventListener("mouseout", function(event) {{
            e.previousElementSibling.childNodes[0].style.color = '';
        }});
    }}
}});
</script>
</body>
</html>"##
        )?;
        Ok(())
    }
}

impl Formatter for MarkdownFormatter {
    #[allow(unused_variables)]
    fn header(&self, output: &mut dyn Write) -> Result<(), Box<dyn Error>> {
        Ok(())
    }

    fn title(&self, output: &mut dyn Write) -> Result<(), Box<dyn Error>> {
        write!(output, "# Rust Compiler Error Index\n")?;
        Ok(())
    }

    fn error_code_block(
        &self,
        output: &mut dyn Write,
        info: &ErrorMetadata,
        err_code: &str,
    ) -> Result<(), Box<dyn Error>> {
        Ok(match info.description {
            Some(ref desc) => write!(output, "## {}\n{}\n", err_code, desc)?,
            None => (),
        })
    }

    #[allow(unused_variables)]
    fn footer(&self, output: &mut dyn Write) -> Result<(), Box<dyn Error>> {
        Ok(())
    }
}

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

    formatter.header(&mut output_file)?;
    formatter.title(&mut output_file)?;

    for (err_code, info) in err_map {
        formatter.error_code_block(&mut output_file, info, err_code)?;
    }

    formatter.footer(&mut output_file)
}

fn main_with_result(format: OutputFormat, dst: &Path) -> Result<(), Box<dyn Error>> {
    let long_codes = register_all();
    let mut err_map = BTreeMap::new();
    for (code, desc) in long_codes {
        err_map.insert(code.to_string(), ErrorMetadata { description: desc.map(String::from) });
    }
    match format {
        OutputFormat::Unknown(s) => panic!("Unknown output format: {}", s),
        OutputFormat::HTML(h) => render_error_page(&err_map, dst, h)?,
        OutputFormat::Markdown(m) => render_error_page(&err_map, dst, m)?,
    }
    Ok(())
}

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

fn main() {
    rustc_driver::init_env_logger("RUST_LOG");
    let (format, dst) = parse_args();
    let result = rustc_span::with_default_session_globals(move || main_with_result(format, &dst));
    if let Err(e) = result {
        panic!("{}", e.to_string());
    }
}

include!(concat!(env!("OUT_DIR"), "/error_codes.rs"));
