use crate::*;

use markdown::*;
use recipe::*;


pub fn generate_html(document: &MarkdownDocument, page: &Page, website: &Website) -> String {
    let root = page.root();
    let page_name = sanitize_text(&page.name);
    let site_name = sanitize_text(&website.name);
    let mut parent_url = String::new();
    for segment in &page.parents {
        parent_url.push_str(&make_url_safe(segment)); parent_url.push('/');
    }
    parent_url.pop();

    let head = get_html_head(page, website); let head = head.trim();
    let home_link = format!("<a id='home' href='{root}index.html'>{site_name}</a>");
    let parent_link = match page.parents.get(page.parents.len()-1) {
        Some(name) => format!("<a id='parent' href='../{}.html'>{name}</a>", make_url_safe(name)),
        None => String::new(),
    };
    let table_of_contents = get_table_of_contents(page);
    let main = document_to_html(document, page, website); let main = main.trim();

    format!("\
<!DOCTYPE html>
<head>
<title>{page_name} &mdash; {site_name}</title>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
{head}
</head>
<body>
<header>
<nav id='up'>
{home_link}
{parent_link}
</nav>
<h1 id='title'>{page_name}</h1>
<nav id='toc'>
{table_of_contents}
</nav>
</header>
<main>
{main}
</main>
</body>
</html>")
}


pub fn generate_html_redirect(path: &str) -> String {
    let path = sanitize_text(path);
    format!("\
<!DOCTYPE html>
<head>
<title>Redirect</title>
<meta http-equiv='refresh' content='0; url={path}'>
</head>
<html>")
}


pub fn get_html_head(page: &Page, website: &Website) -> String {
    let root = page.root();
    website.get_config("html.head")
        .replace("href='/", &format!("href='{root}"))
        .replace("src='/", &format!("src='{root}"))
}


pub fn get_table_of_contents(page: &Page) -> String {
    if page.headings.iter().filter(|h| h.level != Level::Heading3).count() < 3 {
        return String::new();
    }
    let mut toc = String::from("<details><summary></summary><ul>\n");
    let site_name = sanitize_text(&page.name);
    toc.push_str(&format!("<li class='l1'><a href='#title'>{site_name}</a></li>\n"));

    for heading in &page.headings {
        let name = &heading.name;
        let url = &heading.url;
        let class = match heading.level {
            Level::Heading1 => "l1",
            Level::Heading2 => "l2",
            Level::Heading3 => "l3",
        };
        toc.push_str(&format!("<li class='{class}'><a href='#{url}'>{name}</a></li>\n"));
    }
    toc.push_str("</ul></details>\n");
    return toc;
}


pub fn document_to_html(document: &MarkdownDocument, page: &Page, website: &Website) -> String {
    let mut html = String::new();

    macro_rules! line_to_html {
        ($l:expr) => {{ line_to_html(&$l, page, website) }}; }
    macro_rules! html {
        ($($arg:tt)*) => {{ html.push_str(&format!($($arg)*)); html.push('\n'); }}; }
    macro_rules! tag {
        ($t:expr,$l:expr,$c:expr) => { html!("<{} {}>{}</{}>", $t, $c, line_to_html!($l), $t) };
        ($t:expr,$l:expr)         => { html!("<{}>{}</{}>",    $t,     line_to_html!($l), $t) }; }
    macro_rules! wrap {
        ($t:expr,$c:expr,$f:expr) => {{ html!("<{} {}>", $t, $c); $f; html!("</{}>", $t); }};
        ($t:expr,$f:expr)         => {{ html!("<{}>", $t);        $f; html!("</{}>", $t); }}; }

    let root = page.root();
    for block in &document.blocks {
        match block {
            Block::Heading { level, line } => match level {
                Level::Heading1 => tag!("h1", line, format!("id='{}'", make_url_safe(&line_to_html!(line)))),
                Level::Heading2 => tag!("h2", line, format!("id='{}'", make_url_safe(&line_to_html!(line)))),
                Level::Heading3 => tag!("h3", line, format!("id='{}'", make_url_safe(&line_to_html!(line)))),
            }
            Block::Paragraph(line) => {
                if let Some(stripped) = line.to_string().strip_prefix("$$ ") {
                    if let Some(stripped) = stripped.strip_suffix(" $$") {
                        html!("<div class='math'>{stripped}</div>");
                        continue;
                    }
                }
                tag!("p", line);
            }
            Block::List(lines) => wrap!("ul", for line in lines {
                // Insert a <br> tag directly after the first untagged colon.
                let mut depth = 0;
                let mut prev = '\0';
                let mut output = String::new();
                let mut class = String::new();
                for c in line_to_html!(line).chars() {
                    output.push(c);
                    if c == '<' {
                        depth += 1;
                    } else if c == '/' && prev == '<' {
                        depth -= 2;  // 2 because prev was a '<' as well.
                    } else if c == ':' && depth == 0 {
                        output.pop(); output.push_str("<br>");
                        class.push_str("extended"); depth += 99;
                    }
                    prev = c;
                }
                // Replace a leading checkbox with a real checkbox.
                if let Some(stripped) = output.strip_prefix("<code>[ ]</code>") {
                    output = format!("<input type='checkbox' disabled>{stripped}");
                    class.push_str(" checkbox");
                } else if let Some(stripped) = output.strip_prefix("<code>[x]</code>") {
                    output = format!("<input type='checkbox' disabled checked>{stripped}");
                    class.push_str(" checkbox");
                } else if let Some(stripped) = output.strip_prefix("<code>[X]</code>") {
                    output = format!("<input type='checkbox' disabled checked>{stripped}");
                    class.push_str(" checkbox");
                }
                let class = class.trim();
                html!("<li class='{class}'>{output}</li>")
            }),
            Block::Note(lines) => wrap!("aside", for line in lines { tag!("p", line) }),
            Block::Embedded { label, path } => match path.rsplit_once('.') {
                Some((_, extension)) => {
                    let path = match path.strip_prefix('/') {
                        Some(stripped) => format!("{root}{stripped}"),
                        None => path.to_string(),
                    };
                    let label = sanitize_text(label);
                    match extension.to_lowercase().as_str() {
                        "jpg"|"jpeg"|"png"|"webp"|"gif"|"tiff" => html!(
                            "<figure><a href='{path}'><img src='{path}' alt='{label}' title='{label}' /></a></figure>"),
                        "mp3"|"wav"|"m4a" => html!("<audio controls src='{path}'>{label}</audio>"),
                        ext @ _ => warn!("Unrecognised extension for embedded file {path:?} with extension {ext:?} in page {:?}", page.name),
                    }
                }
                _ => warn!("Cannot embed file {path:?} with no file extension in page {:?}", page.name),
            }
            Block::Fragment { language, content } => {
                match language.as_str() {
                    "math" => html!("<div class='math'>{}</div>", content.replace("\n", " \\\\\n")),
                    "embed-html" => html!("{content}"),
                    "embed-css" => wrap!("style", html!("{content}")),
                    "embed-javascript"|"embed-js" => wrap!("script", html!("{content}")),
                    "hidden"|"todo"|"embed-html-head" => (),
                    "recipe" => {
                        let recipe = Recipe::parse(content);
                        html!("<div class='recipe'><ul>");
                        for ingredient in recipe.ingredients { html!("<li>{ingredient}</li>") }
                        html!("</ul><hr>");
                        for paragraph in recipe.process { html!("<p>{paragraph}</p>") }
                        html!("</div>");
                    },
                    "gallery" => wrap!("div", "class='gallery'", for line in content.lines() {
                        let file = line.trim();
                        if !website.has_image(file) {
                            warn!("Gallery on page {:?} references nonexistent image {file:?}", page.name);
                            continue;
                        }
                        let large = format!("{root}images/large/{file}");
                        // let small = format!("{root}images/small/{file}");
                        let thumb = format!("{root}images/thumb/{file}");
                        html!("<a href='{large}'><img src='{thumb}' /></a>");
                    }),
                    _ => {
                        html!("<pre class='{}'>", language);
                        html!("{}", sanitize_text(content));
                        html!("</pre>");
                    },
                }
            }
            Block::Break => html!("<hr>"),
            Block::Table(table) => wrap!("div", "class='table'", wrap!("table", {
                wrap!("thead",
                    wrap!("tr", for column in &table.columns {
                        tag!("th", column.name);
                    })
                );
                for section in &table.sections {
                    wrap!("tbody", for row in section {
                        wrap!("tr", for (column, cell) in std::iter::zip(&table.columns, row) {
                            let text_raw = line_to_html!(cell);
                            let text = match text_raw.as_str() {
                                "Yes" => "✓",
                                "No"  => "✗",
                                other => other,
                            };
                            let align = match text {
                                "--" => "c",
                                _ => match column.alignment {
                                    Alignment::Left => "l",
                                    Alignment::Center => "c",
                                    Alignment::Right => "r",
                                },
                            };
                            let class = match ["No", "--", "0"].contains(&text_raw.as_str()) {
                                true  => format!("{align} dim"),
                                false => format!("{align}"),
                            };
                            html!("<td class='{}'>{}</td>", class, text);
                        })
                    })
                };
            }))
        }
    }
    return html;
}



fn line_to_html(line: &Line, page: &Page, website: &Website) -> String {
    let mut html = String::new();
    for line_element in &line.tokens {
        match line_element {
            Token::Normal(text) => {
                let text = &sanitize_text(text); html.push_str(text) }
            Token::Bold(text) => {
                let text = &sanitize_text(text); html.push_str(&format!("<b>{text}</b>")) }
            Token::Italic(text) => {
                let text = &sanitize_text(text); html.push_str(&format!("<i>{text}</i>")) }
            Token::Monospace(text) => {
                let text = &sanitize_text(text); html.push_str(&format!("<code>{text}</code>")) }
            Token::Math(text) => {
                let text = &sanitize_text(text); html.push_str(&format!("<span class='math'>{text}</span>")) }
            Token::InternalLink(path) => {
                let (label, class, path) = match path.split_once('#') {
                    Some(("", section)) => (section, "heading",  format!("#{section}")),
                    Some((page, section)) => (section, "page", format!("{page}.html#{section}")),
                    _ => (path.as_str(), "page", format!("{path}.html")),
                };
                let mut path = make_url_safe(&path);
                let full_label = sanitize_text(label);
                let label = match full_label.rsplit_once('/') {
                    Some((_parent, label)) => label.trim(),
                    None => &full_label,
                };
                // Check that the linked internal page exists.
                if class == "page" {
                    match website.has_page(page, &path, "html") {
                        Some(resolved_path) => path = resolved_path,
                        None => warn!("Page {:?} contains link to nonexistent page {path:?}", page.name),
                    }
                }
                // Check that the heading exists.
                if class == "heading" {
                    let heading = path.strip_prefix('#').unwrap().to_string();
                    if !page.headings.iter().any(|h| h.url == heading) {
                        warn!("Page {:?} contains link to nonexistent internal heading {heading:?}", page.name);
                    }
                }
                html.push_str(&format!("<a href='{path}' class='{class}'>{label}</a>"))
            }
            Token::ExternalLink { label, path } => {
                let mut path = path.to_owned();
                let mut label = label.to_string();

                let mut is_internal = true;
                for protocol in ["mailto:", "http://", "https://"] {
                    if let Some(stripped) = path.strip_prefix(protocol) {
                        is_internal = false;
                        if label.is_empty() {
                            label = stripped.to_string();
                        }
                    }

                }
                if is_internal {
                    // Check that the linked static file exists.
                    match website.has_static(page, &path) {
                        Some(resolved_path) => path = resolved_path,
                        None => warn!("Page {:?} contains link to nonexistent static file {path:?}", page.name),
                    }
                    // Take the file name as the label if the link is unlabeled.
                    if label.is_empty() {
                        label = match path.rsplit_once('/') {
                            Some((_, file)) => file.to_string(),
                            None => path.clone(),
                        };
                    }
                }

                let label = sanitize_text(&label);
                html.push_str(&format!("<a href='{path}' class='external'>{label}</a>"));
            }
        }
    }
    return html;
}



/// Replace each HTML-reserved character with an HTML-escaped character.
fn sanitize_text(text: &str) -> String {
    let mut output = String::new();
    let chars: Vec<char> = text.chars().collect();
    for (i, c) in chars.iter().enumerate() {
        let prev = match i > 0 {
            true => chars[i - 1],
            false => ' ',
        };
        let next = match i + 1 < chars.len() {
            true => chars[i + 1],
            false => ' ',
        };
        match c {
            '&' => {
                // The HTML syntax for unicode characters is &#0000
                if let Some('#') = chars.get(i+1) { output.push(*c) }
                else { output.push_str("&amp;") }
            },
            '<' => output.push_str("&lt;"),
            '>' => output.push_str("&gt;"),
            '"' => match prev.is_whitespace() {
                true  => output.push('“'),
                false => output.push('”'),
            },
            '\'' => match prev.is_whitespace() {
                true  => output.push('‘'),
                false => output.push('’'),
            },
            '-' => match prev.is_whitespace() && next.is_whitespace() {
                true => output.push('—'),
                false => output.push('-'),
            }
            _ => output.push(*c),
        }
    }
    return output;
}