use pulldown_cmark as md;

/// Converts a given string of Markdown to semi-equivalent gemtext.
///
/// # Panics
///
/// Will panic if gemtext::render somehow produces invalid UTF-8.
/// Since gemtext::render only produces valid UTF-8, this should never happen.
pub fn convert(markdown_text: &str) -> String {
    let parser = md::Parser::new_ext(markdown_text, md::Options::empty());
    let mut state = State::new();

    for event in parser {
        match event {
            md::Event::Start(md::Tag::Paragraph) => (),
            md::Event::Start(md::Tag::Heading(level)) => state.start_heading(level),
            md::Event::Start(md::Tag::BlockQuote) => state.start_block_quote(),
            md::Event::Start(md::Tag::CodeBlock(_)) => state.start_code_block(),
            md::Event::Start(md::Tag::List(_)) => (),
            md::Event::Start(md::Tag::Item) => state.start_list_item(),
            md::Event::Start(md::Tag::FootnoteDefinition(_)) => unimplemented!("footnotes disabled"),
            md::Event::Start(md::Tag::Table(_)) => unimplemented!("tables disabled"),
            md::Event::Start(md::Tag::TableHead) => unimplemented!("tables disabled"),
            md::Event::Start(md::Tag::TableRow) => unimplemented!("tables disabled"),
            md::Event::Start(md::Tag::TableCell) => unimplemented!("tables disabled"),
            md::Event::Start(md::Tag::Emphasis) => state.toggle_emphasis(),
            md::Event::Start(md::Tag::Strong) => state.toggle_strong(),
            md::Event::Start(md::Tag::Strikethrough) => unimplemented!("strikethrough disabled"),
            md::Event::Start(md::Tag::Link(_, _, _)) => state.start_link(),
            md::Event::Start(md::Tag::Image(_, _, _)) => state.start_image(),

            md::Event::End(md::Tag::Paragraph) => state.finish_node(),
            md::Event::End(md::Tag::Heading(_)) => state.finish_node(),
            md::Event::End(md::Tag::BlockQuote) => (),
            md::Event::End(md::Tag::CodeBlock(_)) => state.finish_node(),
            md::Event::End(md::Tag::List(_)) => state.finish_list(),
            md::Event::End(md::Tag::Item) => state.finish_node(),
            md::Event::End(md::Tag::FootnoteDefinition(_)) => unimplemented!("footnotes disabled"),
            md::Event::End(md::Tag::Table(_)) => unimplemented!("tables disabled"),
            md::Event::End(md::Tag::TableHead) => unimplemented!("tables disabled"),
            md::Event::End(md::Tag::TableRow) => unimplemented!("tables disabled"),
            md::Event::End(md::Tag::TableCell) => unimplemented!("tables disabled"),
            md::Event::End(md::Tag::Emphasis) => state.toggle_emphasis(),
            md::Event::End(md::Tag::Strong) => state.toggle_strong(),
            md::Event::End(md::Tag::Strikethrough) => unimplemented!("strikethrough disabled"),
            md::Event::End(md::Tag::Link(_, href, _)) => state.end_link(&href),
            md::Event::End(md::Tag::Image(_, src, _)) => state.end_image(&src),

            md::Event::Text(text) => state.add_text(&text),
            md::Event::Code(code) => state.add_inline_code(&code),
            md::Event::Html(html) => state.add_text(&html),
            md::Event::FootnoteReference(_) => unimplemented!("footnotes disabled"),
            md::Event::SoftBreak => state.add_text(" "),
            md::Event::HardBreak => state.finish_node(),
            md::Event::Rule => state.add_rule(),
            md::Event::TaskListMarker(_) => unimplemented!("task lists disabled"),
        }
    }

    let nodes = state.nodes
        .into_iter()
        .filter(|cluster| !cluster.is_empty())
        .map(condense)
        .collect::<Vec<_>>()
        .join(&gemtext::Node::blank());
    let mut result: Vec<u8> = vec![];
    gemtext::render(nodes, &mut result).expect("gemtext::render somehow failed");
    String::from_utf8(result).expect("gemtext::render somehow produced invalid UTF-8")
}

type NodeCluster = Vec<gemtext::Node>;

fn condense(original: NodeCluster) -> NodeCluster {
    match original.as_slice() {
        [gemtext::Node::Text(text), gemtext::Node::Link { name: Some(name), .. }] if text == name => vec![original[1].clone()],
        _ => original,
    }
}

enum NodeType {
    Text,
    Preformatted,
    Heading { level: u8 },
    ListItem,
    Quote,
}

impl NodeType {
    fn take(&mut self) -> Self {
        std::mem::replace(self, NodeType::Text)
    }

    fn construct(self, body: String) -> gemtext::Node {
        use NodeType::*;
        match self {
            Text => gemtext::Node::Text(body),
            Preformatted => gemtext::Node::Preformatted(body),
            Heading { level } => gemtext::Node::Heading { level, body },
            ListItem => gemtext::Node::ListItem(body),
            Quote => gemtext::Node::Quote(body),
        }
    }
}

struct State {
    nodes: Vec<NodeCluster>,
    pending_node_content: String,
    pending_node_type: NodeType,
    pending_other: Vec<gemtext::Node>,
    link_text_stack: Vec<String>,
}

impl State {
    fn new() -> Self {
        State {
            nodes: vec![],
            pending_node_content: String::new(),
            pending_node_type: NodeType::Text,
            pending_other: vec![],
            link_text_stack: vec![],
        }
    }

    fn start_heading(&mut self, level: u32) {
        let level = match level {
            1 => 1,
            2 => 2,
            _ => 3,
        };
        self.pending_node_type = NodeType::Heading { level };
    }

    fn start_block_quote(&mut self) {
        self.pending_node_type = NodeType::Quote;
    }

    fn start_code_block(&mut self) {
        self.pending_node_type = NodeType::Preformatted;
    }

    fn start_list_item(&mut self) {
        self.pending_node_type = NodeType::ListItem;
    }

    fn toggle_emphasis(&mut self) {
        self.add_text("_");
    }

    fn toggle_strong(&mut self) {
        self.add_text("**");
    }

    fn start_link(&mut self) {
        self.link_text_stack.push(String::new());
    }

    fn start_image(&mut self) {
        self.link_text_stack.push(String::new());
        self.pending_node_content += "[image: ";
    }

    fn finish_list(&mut self) {
        self.nodes.push(vec![]);
    }

    fn end_link(&mut self, href: &str) {
        let text = self.link_text_stack.pop().unwrap_or_else(|| href.to_string());
        self.pending_other.push(gemtext::Node::Link { to: href.to_string(), name: Some(text) });
    }

    fn end_image(&mut self, src: &str) {
        let text = self.link_text_stack.pop().unwrap_or_else(|| src.to_string());
        let text = format!("[image: {}]", text);
        self.pending_other.push(gemtext::Node::Link { to: src.to_string(), name: Some(text) });
        self.pending_node_content += "]";
    }

    // will create an empty paragraph if pending_text is empty
    fn finish_node(&mut self) {
        match (&self.pending_node_type, self.nodes.last().and_then(|cluster| cluster.last())) {
            (NodeType::ListItem, Some(gemtext::Node::ListItem(_))) => (),
            _ => self.nodes.push(vec![]),
        }
        let node_text = self.pending_node_content.trim().to_string();
        let new_node = self.pending_node_type.take().construct(node_text);
        let last_cluster = self.nodes.last_mut().expect("empty cluster list??");
        last_cluster.push(new_node);
        last_cluster.extend(self.pending_other.drain(..));

        self.pending_node_content = String::new();
    }

    fn add_text(&mut self, text: &str) {
        for link_text in &mut self.link_text_stack {
            *link_text += text;
        }
        self.pending_node_content += text;
    }

    fn add_inline_code(&mut self, code: &str) {
        self.pending_node_content += "`";
        self.pending_node_content += code;
        self.pending_node_content += "`";
    }

    fn add_rule(&mut self) {
        self.add_text("-----");
        self.finish_node();
    }
}

#[cfg(test)]
#[test]
fn run_tests() {
    let markdown_demo = r#"
# h1
## h2
### h3

---

```
sample
  text
```

> implying

1. don't pick up the phone
2. don't let him in
3. don't be his friend

some `code` and some `` fancy`code `` and *italics*
and __bold__ and ***semi-overlapping* bold *and* italics**

this [paragraph](http://example.com) has [several links](http://example.org)
and an ![inline image](a://url) in it

![this one's just an image](https://placekitten.com/200/300)
"#;
    let gemtext_demo = r#"# h1

## h2

### h3

-----

```
sample
  text
```

> implying

* don't pick up the phone
* don't let him in
* don't be his friend

some `code` and some `fancy`code` and _italics_ and **bold** and **_semi-overlapping_ bold _and_ italics**

this paragraph has several links and an [image: inline image] in it
=> http://example.com paragraph
=> http://example.org several links
=> a://url [image: inline image]

=> https://placekitten.com/200/300 [image: this one's just an image]
"#;
    assert_eq!(convert(markdown_demo), gemtext_demo);
}

#[cfg(test)]
#[test]
fn test_list_start() {
    let markdown = "> hi\n\n1. uh\n2. ah\n";
    let gemtext = "> hi\n\n* uh\n* ah\n";
    assert_eq!(convert(markdown), gemtext);
}