use crate::util::slugify;
use pulldown_cmark::{
    Alignment, Event, HeadingLevel, Options, Parser, Tag, TagEnd,
};
use std::{
    collections::HashSet, fs::File, io::prelude::*, path::Path, process,
};

static INDEX_PREAMBLE: &[u8] = br##"<!DOCTYPE html>
<html dir="ltr" lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Oddjobs | Guides</title>
    <meta name="description" content="The guild for MapleStory odd-jobbers" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />

    <link rel="stylesheet" type="text/css" href="../css/style.css" />
    <link rel="icon" type="image/png" href="../img/favicon.png" />
  </head>

  <body>
    <div id="nav-content-wrapper">
      <nav id="main-nav">
        <div id="main-nav-inner">
          <div id="main-nav-pullout"><span>&#x25b6;</span></div>

          <a href="../" class="nav-h"
            >Oddjobs
            <br />
            <img
              src="../img/logo.svg"
              alt="Oddjobs logo | Due to Evan MacDonald of the Noun Project (CC BY 3.0)"
              title="Oddjobs logo | Due to Evan MacDonald of the Noun Project (CC BY 3.0)"
          /></a>

          <ul class="nav-list">
            <li>
              <a href="../">Home</a>
            </li>
            <li>
              <a href="../join-on-up.html">Join On Up</a>
            </li>
            <li>
              <a
                target="_blank"
                rel="noopener noreferrer"
                href="https://oddjobs.flarum.cloud/"
                >Forum</a
              >
            </li>
            <li>
              <a href="../odd-jobs.html">Odd Jobs</a>
            </li>
            <li class="active">
              <a href="./">Guides</a>
            </li>
            <li>
              <a href="../dmg-calc/">Damage Calc</a>
            </li>
            <li>
              <a href="../gish-ap-calc/">Gish AP Calc</a>
            </li>
            <li>
              <a href="../archive/">Archive</a>
            </li>
            <li>
              <a href="../rankings/">Rankings</a>
            </li>
            <li>
              <a href="../source.html" data-jslicense="1">Source</a>
            </li>
          </ul>
        </div>
      </nav>

      <div id="content">
        <header>
          <h1>Guides</h1>
          <span class="subtitle"
            >The most esoteric videogame guides on the internet!</span
          >
        </header>

        <main>
          <ul>
"##;

static INDEX_POSTAMBLE: &[u8] = br##"          </ul>
        </main>
      </div>
    </div>
  </body>
</html>
"##;

static PREAMBLE0: &str = r##"<!DOCTYPE html>
<html dir="ltr" lang="{{ LANG }}">
  <head>
    <meta charset="UTF-8" />
    <title>Oddjobs | Guides | "##;

static PREAMBLE1: &[u8] = br##"</title>
    <meta name="description" content="The guild for MapleStory odd-jobbers" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />

    <link rel="stylesheet" type="text/css" href="../../css/style.css" />
    <link rel="icon" type="image/png" href="../../img/favicon.png" />
  </head>

  <body>
    <div id="nav-content-wrapper">
      <nav id="main-nav">
        <div id="main-nav-inner">
          <div id="main-nav-pullout"><span>&#x25b6;</span></div>

          <a href="../../" class="nav-h"
            >Oddjobs
            <br />
            <img
              src="../../img/logo.svg"
              alt="Oddjobs logo | Due to Evan MacDonald of the Noun Project (CC BY 3.0)"
              title="Oddjobs logo | Due to Evan MacDonald of the Noun Project (CC BY 3.0)"
          /></a>

          <ul class="nav-list">
            <li>
              <a href="../../">Home</a>
            </li>
            <li>
              <a href="../../join-on-up.html">Join On Up</a>
            </li>
            <li>
              <a
                target="_blank"
                rel="noopener noreferrer"
                href="https://oddjobs.flarum.cloud/"
                >Forum</a
              >
            </li>
            <li>
              <a href="../../odd-jobs.html">Odd Jobs</a>
            </li>
            <li class="active">
              <a href="../">Guides</a>
            </li>
            <li>
              <a href="../../dmg-calc/">Damage Calc</a>
            </li>
            <li>
              <a href="../../gish-ap-calc/">Gish AP Calc</a>
            </li>
            <li>
              <a href="../../archive/">Archive</a>
            </li>
            <li>
              <a href="../../rankings/">Rankings</a>
            </li>
            <li>
              <a href="../../source.html" data-jslicense="1">Source</a>
            </li>
          </ul>
        </div>
      </nav>

      <div id="content">
        <header>
          <!-- The HTML below this comment is automatically generated from a
          Markdown file by guide.rs.  See: <https://codeberg.org/oddjobs/pages>
          -->
"##;

static POSTAMBLE: &[u8] = br##"
        </main>
      </div>
    </div>
  </body>
</html>
"##;

static RANKINGS_PREAMBLE: &[u8] = br##"<!DOCTYPE html>
<html dir="ltr" lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Oddjobs | Rankings</title>
    <meta name="description" content="The guild for MapleStory odd-jobbers" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />

    <link rel="stylesheet" type="text/css" href="../css/style.css" />
    <link rel="icon" type="image/png" href="../img/favicon.png" />
  </head>

  <body>
    <div id="nav-content-wrapper">
      <nav id="main-nav">
        <div id="main-nav-inner">
          <div id="main-nav-pullout"><span>&#x25b6;</span></div>

          <a href="../" class="nav-h"
            >Oddjobs
            <br />
            <img
              src="../img/logo.svg"
              alt="Oddjobs logo | Due to Evan MacDonald of the Noun Project (CC BY 3.0)"
              title="Oddjobs logo | Due to Evan MacDonald of the Noun Project (CC BY 3.0)"
          /></a>

          <ul class="nav-list">
            <li>
              <a href="../">Home</a>
            </li>
            <li>
              <a href="../join-on-up.html">Join On Up</a>
            </li>
            <li>
              <a
                target="_blank"
                rel="noopener noreferrer"
                href="https://oddjobs.flarum.cloud/"
                >Forum</a
              >
            </li>
            <li>
              <a href="../odd-jobs.html">Odd Jobs</a>
            </li>
            <li>
              <a href="../guides/">Guides</a>
            </li>
            <li>
              <a href="../dmg-calc/">Damage Calc</a>
            </li>
            <li>
              <a href="../gish-ap-calc/">Gish AP Calc</a>
            </li>
            <li>
              <a href="../archive/">Archive</a>
            </li>
            <li class="active">
              <a href="./">Rankings</a>
            </li>
            <li>
              <a href="../source.html" data-jslicense="1">Source</a>
            </li>
          </ul>
        </div>
      </nav>

      <div id="content">
        <header>
          <!-- The HTML below this comment is automatically generated from a
          Markdown file by guide.rs.  See: <https://codeberg.org/oddjobs/pages>
          -->
"##;

pub fn render_index<P: AsRef<Path>, W: Write>(input_dir_path: P, out: &mut W) {
    out.write_all(INDEX_PREAMBLE).unwrap();

    for dir_entry in input_dir_path
        .as_ref()
        .read_dir()
        .expect("Could not read `input_dir_path` as a directory")
    {
        let dir_entry = dir_entry.unwrap();
        let dir_entry_name =
            dir_entry.file_name().into_string().expect("Bad `OsString`");

        if dir_entry.file_type().unwrap().is_dir()
            && !dir_entry_name.starts_with('.')
        {
            write!(
                out,
                r##"            <li><a href="./{dir_entry_name}/">"##,
            )
            .unwrap();

            let mut main_readme_path = dir_entry.path();
            main_readme_path.push("README.md");

            write_title(out, &main_readme_path);

            out.write_all(b"</a>").unwrap();

            let mut needs_sublist = true;
            for subdir_entry in dir_entry
                .path()
                .read_dir()
                .expect("Could not read subdir as a directory")
            {
                let subdir_entry = subdir_entry.unwrap();

                if subdir_entry.file_type().unwrap().is_file()
                    && subdir_entry
                        .path()
                        .extension()
                        .map(|oss| oss.to_str().unwrap())
                        == Some("md")
                    && subdir_entry.file_name() != "README.md"
                {
                    let readme_path = subdir_entry.path();

                    write!(
                        out,
                        r##"{}<li><a href="./{}/index.{}.html">"##,
                        if needs_sublist { "<ul>" } else { "" },
                        dir_entry_name,
                        subdir_entry
                            .file_name()
                            .to_str()
                            .expect("Filename is not UTF-8‽")
                            .rsplit('.')
                            .nth(1)
                            .unwrap(),
                    )
                    .unwrap();
                    needs_sublist = false;

                    write_title(out, &readme_path);

                    out.write_all(b"</a></li>\n").unwrap();
                }
            }
            if !needs_sublist {
                out.write_all(b"</ul>").unwrap();
            }

            out.write_all(b"</li>\n").unwrap();
        }
    }

    out.write_all(INDEX_POSTAMBLE).unwrap();
}

fn write_title<W: Write, P: AsRef<Path>>(out: &mut W, readme_path: P) {
    let mut input_file = File::open(&readme_path)
        .expect("Could not open README*.md file for reading");
    let mut input_str = String::new();
    input_file
        .read_to_string(&mut input_str)
        .expect("Could not read README*.md file into memory");

    let peek_parser = Parser::new(
        input_str.split('\n').next().expect("No newlines in file"),
    );
    let mut first_event = true;
    for event in peek_parser {
        if first_event
            && !matches!(
                event,
                Event::Start(Tag::Heading {
                    level: HeadingLevel::H1,
                    ..
                })
            )
        {
            eprintln!(
                "Expected first element of {:?} to be a top-level heading",
                readme_path.as_ref(),
            );

            process::exit(1)
        }
        first_event = false;

        match event {
            Event::Start(tag) => match tag {
                Tag::Heading {
                    level: HeadingLevel::H1,
                    ..
                } => (),
                Tag::Emphasis => {
                    out.write_all(b"<em>").unwrap();
                }
                Tag::Strong => {
                    out.write_all(b"<strong>").unwrap();
                }
                Tag::Strikethrough => {
                    out.write_all(b"<del>").unwrap();
                }
                _ => {
                    eprintln!("Unexpected element in top-level heading");

                    process::exit(1)
                }
            },
            Event::Text(s) => {
                out.write_all(s.as_bytes()).unwrap();
            }
            Event::Code(s) => {
                out.write_all(b"<code>").unwrap();
                out.write_all(s.as_bytes()).unwrap();
                out.write_all(b"</code>").unwrap();
            }
            Event::End(tag) => match tag {
                TagEnd::Emphasis => {
                    out.write_all(b"</em>").unwrap();
                }
                TagEnd::Strong => {
                    out.write_all(b"</strong>").unwrap();
                }
                TagEnd::Strikethrough => {
                    out.write_all(b"</del>").unwrap();
                }
                _ => break,
            },
            _ => break,
        }
    }
}

pub fn render<P: AsRef<Path>, W: Write>(
    input_file_path: P,
    rankings: bool,
    out: &mut W,
) {
    let mut input_file = File::open(&input_file_path)
        .expect("Could not open input file for reading");
    let mut input_str = String::new();
    input_file
        .read_to_string(&mut input_str)
        .expect("Could not read input file into memory");

    let title_slug = if rankings {
        out.write_all(RANKINGS_PREAMBLE).unwrap();

        String::new()
    } else {
        let lang: Vec<_> = input_file_path
            .as_ref()
            .file_name()
            .expect("No filename…!")
            .to_str()
            .expect("Filename is not UTF-8‽")
            .rsplit('.')
            .collect();

        out.write_all(
            PREAMBLE0
                .replace(
                    "{{ LANG }}",
                    if lang.len() == 2 { "en" } else { lang[1] },
                )
                .as_bytes(),
        )
        .unwrap();

        let peek_parser = Parser::new(
            input_str.split('\n').next().expect("No newlines in file"),
        );
        let mut title_slug = String::new();
        let mut first_event = true;
        for event in peek_parser {
            if first_event
                && !matches!(
                    event,
                    Event::Start(Tag::Heading {
                        level: HeadingLevel::H1,
                        ..
                    })
                )
            {
                eprintln!("Expected first element to be a top-level heading");

                process::exit(1)
            }
            first_event = false;

            match event {
                Event::Start(tag) => match tag {
                    Tag::Heading {
                        level: HeadingLevel::H1,
                        ..
                    }
                    | Tag::Emphasis
                    | Tag::Strong
                    | Tag::Strikethrough => (),
                    _ => {
                        eprintln!("Unexpected element in top-level heading");

                        process::exit(1)
                    }
                },
                Event::Text(s) => {
                    out.write_all(s.as_bytes()).unwrap();
                    title_slug.push_str(&slugify(&s));
                }
                Event::Code(s) => {
                    out.write_all(s.as_bytes()).unwrap();
                    title_slug.push_str(&slugify(&s));
                }
                Event::End(tag) => match tag {
                    TagEnd::Emphasis
                    | TagEnd::Strong
                    | TagEnd::Strikethrough => (),
                    _ => break,
                },
                _ => break,
            }
        }

        out.write_all(PREAMBLE1).unwrap();

        title_slug
    };

    let parser = Parser::new_ext(&input_str, Options::ENABLE_TABLES);

    enum NestedHeadingTag {
        Em,
        EmClose,
    }
    enum HeadingContent {
        Text(String),
        Nested(NestedHeadingTag),
    }

    let mut seen_h1 = false;
    let mut in_heading = 0;
    let mut heading_content = Vec::new();
    let mut table_alignments = Vec::new();
    let mut in_thead = false;
    let mut table_cell_ix = 0;
    let mut in_img = false;
    let (mut wanting_p, mut wanting_close_p) = (false, true);
    let mut wanting_a: Option<pulldown_cmark::CowStr> = None;
    let mut used_slugs = HashSet::new();
    for event in parser {
        match event {
            Event::Text(_)
            | Event::Start(Tag::Emphasis)
            | Event::End(TagEnd::Emphasis) => (),
            _ => {
                if !heading_content.is_empty() {
                    if in_heading != 0 {
                        let slug = {
                            let mut s = String::new();
                            for hc in heading_content.iter() {
                                match hc {
                                    HeadingContent::Nested(_) => (),
                                    HeadingContent::Text(text) => {
                                        s.push_str(&slugify(text))
                                    }
                                }
                            }

                            let mut i = 1usize;
                            while used_slugs.contains(&s) {
                                if i == 1 {
                                    s.push_str("-1");
                                } else {
                                    s.truncate(
                                        s.trim_end_matches(char::is_numeric)
                                            .len(),
                                    );
                                    s.push_str(&i.to_string());
                                }

                                i += 1;
                            }

                            s
                        };
                        used_slugs.insert(slug.clone());

                        if in_heading != 1 {
                            write!(
                                out,
                                r##"<a href="#{slug}" class="h-anchor">"##,
                            )
                            .unwrap();
                        }

                        write!(out, r##"<h{in_heading} id="{slug}">"##)
                            .unwrap();
                    }

                    if !in_img {
                        for hc in heading_content.iter() {
                            match hc {
                                HeadingContent::Nested(nht) => match nht {
                                    NestedHeadingTag::Em => {
                                        out.write_all(b"<em>").unwrap()
                                    }
                                    NestedHeadingTag::EmClose => {
                                        out.write_all(b"</em>").unwrap()
                                    }
                                },
                                HeadingContent::Text(s) => out
                                    .write_all(html_esc(s).as_bytes())
                                    .unwrap(),
                            }
                        }
                    }

                    heading_content.truncate(0);
                }
            }
        }

        if wanting_p {
            match event {
                Event::Start(Tag::Link { .. }) => (),
                _ => {
                    if let Event::Start(Tag::Image { .. }) = event {
                        wanting_close_p = false;
                    } else {
                        out.write_all(b"<p>").unwrap();
                    }
                    wanting_p = false;

                    if let Some(url) = wanting_a.take() {
                        write!(out, r##"<a href="{url}">"##).unwrap();
                    }
                }
            }
        }

        match event {
            Event::Start(tag) => match tag {
                Tag::Paragraph => wanting_p = true,
                Tag::Heading { level, .. } => {
                    if level == HeadingLevel::H1 {
                        if seen_h1 {
                            eprintln!("Multiple `<h1>`s are not allowed");

                            process::exit(1);
                        }
                        seen_h1 = true;
                    }

                    in_heading = level as u8;
                }
                Tag::BlockQuote => out.write_all(b"<blockquote>").unwrap(),
                Tag::CodeBlock(_) => out.write_all(b"<pre>").unwrap(),
                Tag::List(ord) => out
                    .write_all(if ord.is_some() { b"<ol>" } else { b"<ul>" })
                    .unwrap(),
                Tag::Item => out.write_all(b"<li>").unwrap(),
                Tag::FootnoteDefinition(_) => unimplemented!(),
                Tag::Table(alignments) => {
                    out.write_all(br##"<div class="table-wrapper"><table>"##)
                        .unwrap();
                    table_alignments = alignments;
                }
                Tag::TableHead => {
                    out.write_all(b"<thead><tr>").unwrap();
                    in_thead = true;
                    table_cell_ix = 0;
                }
                Tag::TableRow => {
                    out.write_all(b"<tr>").unwrap();
                    table_cell_ix = 0;
                }
                Tag::TableCell => {
                    write!(
                        out,
                        r##"<t{} class="align-{}">"##,
                        if in_thead { r##"h scope="col""## } else { "d" },
                        match table_alignments[table_cell_ix] {
                            Alignment::None => "none",
                            Alignment::Left => "left",
                            Alignment::Center => "center",
                            Alignment::Right => "right",
                        },
                    )
                    .unwrap();
                    table_cell_ix += 1;
                }
                Tag::Emphasis => {
                    heading_content
                        .push(HeadingContent::Nested(NestedHeadingTag::Em));
                }
                Tag::Strong => out.write_all(b"<strong>").unwrap(),
                Tag::Strikethrough => out.write_all(b"<del>").unwrap(),
                Tag::Link { dest_url, .. } => {
                    if wanting_p {
                        wanting_a = Some(dest_url);
                    } else {
                        write!(out, r##"<a href="{dest_url}">"##).unwrap();
                    }
                }
                Tag::Image {
                    dest_url, title, ..
                } => {
                    write!(
                        out,
                        r##"<figure><img loading="lazy" src="{dest_url}"
                            alt="{title}" title="{title}" />"##,
                    )
                    .unwrap();
                    in_img = true;
                }
                Tag::MetadataBlock(_) | Tag::HtmlBlock => (),
            },
            Event::End(tag) => match tag {
                TagEnd::Paragraph => {
                    if wanting_close_p {
                        out.write_all(b"</p>").unwrap();
                    }
                    wanting_p = false;
                    wanting_close_p = true;
                }
                TagEnd::Heading(level) => {
                    write!(out, "</h{}>", level as u8).unwrap();

                    if in_heading != 1 {
                        out.write_all(b"</a>").unwrap();
                    }

                    if level == HeadingLevel::H1 {
                        if rankings {
                            out.write_all(br##"</header><main>"##).unwrap();
                        } else {
                            out.write_all(
                                br##"<a href="../"
                                 class="go-back">&#x2190;&nbsp;Back to
                                 Guides&nbsp;&#x2190;</a></header><main>"##,
                            )
                            .unwrap();
                        }
                    } else if !rankings {
                        write!(
                            out,
                            r##"<a href="#{title_slug}" class="go-back"
                                >&#x2191;&nbsp;Back to top&nbsp;&#x2191;</a
                                >"##,
                        )
                        .unwrap();
                    }

                    in_heading = 0;
                }
                TagEnd::BlockQuote => out.write_all(b"</blockquote>").unwrap(),
                TagEnd::CodeBlock => out.write_all(b"</pre>").unwrap(),
                TagEnd::List(is_ord) => out
                    .write_all(if is_ord { b"</ol>" } else { b"</ul>" })
                    .unwrap(),
                TagEnd::Item => out.write_all(b"</li>").unwrap(),
                TagEnd::FootnoteDefinition => unimplemented!(),
                TagEnd::Table => {
                    out.write_all(b"</tbody></table></div>").unwrap()
                }
                TagEnd::TableHead => {
                    out.write_all(b"</tr></thead><tbody>").unwrap();
                    in_thead = false;
                }
                TagEnd::TableRow => out.write_all(b"</tr>").unwrap(),
                TagEnd::TableCell => out
                    .write_all(if in_thead { b"</th>" } else { b"</td>" })
                    .unwrap(),
                TagEnd::Emphasis => {
                    heading_content.push(HeadingContent::Nested(
                        NestedHeadingTag::EmClose,
                    ));
                }
                TagEnd::Strong => out.write_all(b"</strong>").unwrap(),
                TagEnd::Strikethrough => out.write_all(b"</del>").unwrap(),
                TagEnd::Link { .. } => {
                    out.write_all(b"</a>").unwrap();
                    wanting_a = None;
                }
                TagEnd::Image { .. } => {
                    out.write_all(b"</figure>").unwrap();
                    in_img = false;
                }
                TagEnd::MetadataBlock(_) | TagEnd::HtmlBlock => (),
            },
            Event::Text(s) => {
                heading_content.push(HeadingContent::Text(s.into_string()))
            }
            Event::Code(s) => {
                write!(out, "<code>{}</code>", html_esc(&s)).unwrap();
            }
            Event::Html(s) => out.write_all(s.as_bytes()).unwrap(),
            Event::InlineHtml(s) => out.write_all(s.as_bytes()).unwrap(),
            Event::FootnoteReference(_) => unimplemented!(),
            Event::SoftBreak | Event::HardBreak => {
                out.write_all(b" ").unwrap();
            }
            Event::Rule => out.write_all(b"<hr />").unwrap(),
            Event::TaskListMarker(_) => unimplemented!(),
        }
    }

    out.write_all(POSTAMBLE).unwrap();
}

fn html_esc(s: &str) -> String {
    let mut escaped = String::with_capacity(s.len() + 4);

    for c in s.chars() {
        match c {
            '&' => escaped.push_str("&amp;"),
            '<' => escaped.push_str("&lt;"),
            '>' => escaped.push_str("&gt;"),
            _ => escaped.push(c),
        }
    }

    escaped
}
