#![feature(never_type)]

mod block; pub use block::{Block, Level};
mod line;  pub use line::Line;
mod token; pub use token::Token;
mod table; pub use table::{Table, Column, Alignment};

pub struct MarkdownDocument {
    pub blocks: Vec<Block>,
}

impl MarkdownDocument {
    pub fn from_str(raw_markdown: &str) -> Self {
        let mut blocks = Vec::new();
        let mut current_block = None;

        // Chain a blank line to the end to ensure the final block is flushed.
        for line in raw_markdown.lines().chain(std::iter::once("")) {
            let line_raw = line;
            let line = line.trim();

            // Handle a fragment block separately, because fragment lines are not prefixed.
            if let Some(BlockMultiline::Fragment { language, mut content }) = current_block {
                if line == "```" {
                    let language = language.to_string();
                    let content = content.join("\n");
                    blocks.push(Block::Fragment { language, content });
                    current_block = None;
                } else {
                    content.push(line_raw);
                    current_block = Some(BlockMultiline::Fragment { language, content });
                }
                continue;
            }

            // Determine line type from prefix.
            let line = {
                if let Some(("", tail)) = line.split_once("# ") {
                    BlockLine::Heading { level: Level::Heading1, line: tail.trim() }
                } else if let Some(("", tail)) = line.split_once("## ") {
                    BlockLine::Heading { level: Level::Heading2, line: tail.trim() }
                } else if let Some(("", tail)) = line.split_once("### ") {
                    BlockLine::Heading { level: Level::Heading3, line: tail.trim() }
                } else if let Some(("", tail)) = line.split_once("- ") {
                    BlockLine::List(tail.trim())
                } else if let Some(("", tail)) = line.split_once("> ") {
                    BlockLine::Note(tail.trim())
                } else if line == ">" {
                    BlockLine::Note("")
                } else if let Some(("", tail)) = line.split_once("```") {
                    BlockLine::FragmentHeader(tail.trim())
                } else if line.starts_with("|") {
                    BlockLine::Table(line)
                } else if line.len() >= 3 && line.chars().all(|c| c=='-') {
                    BlockLine::Break
                } else if line.is_empty() {
                    BlockLine::BlankLine
                } else {
                    BlockLine::Paragraph(line)
                }
            };

            // If line has the same type as the current block, append and continue.
            if let Some(ref mut block) = current_block {
                match (&line, block)  {
                    (BlockLine::List(line), BlockMultiline::List(ref mut lines)) => {
                        lines.push(line); continue; }
                    (BlockLine::Note(line), BlockMultiline::Note(ref mut lines)) => {
                        lines.push(line); continue; }
                    (BlockLine::Table(line), BlockMultiline::Table(ref mut lines)) => {
                        lines.push(line); continue; }
                    _ => (),
                };
            }

            // Otherwise commit the current block before handling the new line.
            if let Some(current_block) = std::mem::take(&mut current_block) {
                match current_block {
                    BlockMultiline::List(raw_lines) => {
                        let lines = raw_lines.into_iter().map(Line::from_str).collect();
                        blocks.push(Block::List(lines)); }
                    BlockMultiline::Note(raw_lines) => {
                        let lines = raw_lines.into_iter().map(Line::from_str).collect();
                        blocks.push(Block::Note(lines)); }
                    BlockMultiline::Table(raw_lines) => {
                        if let Some(table) = Table::from_strs(&raw_lines) {
                            blocks.push(Block::Table(table)) }
                        else {
                            for raw_line in raw_lines {
                                blocks.push(Block::Paragraph(Line::from_str(&raw_line)))
                            }
                        }}
                    BlockMultiline::Fragment {..} => unreachable!(),
                }
            }

            // Handle the new line.
            match line {
                BlockLine::List(line)  => current_block = Some(
                    BlockMultiline::List(vec![line])),
                BlockLine::Note(line)  => current_block = Some(
                    BlockMultiline::Note(vec![line])),
                BlockLine::Table(line) => current_block = Some(
                    BlockMultiline::Table(vec![line])),
                BlockLine::FragmentHeader(language) => current_block = Some(
                    BlockMultiline::Fragment { language, content: Vec::new() }),
                BlockLine::Heading {level, line} => blocks.push(
                    Block::Heading { level, line: Line::from_str(&line) }),
                BlockLine::Break => blocks.push(Block::Break),
                BlockLine::BlankLine => (),
                BlockLine::Paragraph(line) => if let Some(embed) = parse_embed(&line) {
                    blocks.push(embed)
                } else if let Some(math) = parse_math(&line) {
                    blocks.push(math)
                } else {
                    blocks.push(Block::Paragraph(Line::from_str(&line)))
                }
            }
        }

        Self { blocks }
    }
}



enum BlockLine<'a> {
    Heading { level: Level, line: &'a str },
    Paragraph(&'a str),
    List(&'a str),
    Note(&'a str),
    Table(&'a str),
    FragmentHeader(&'a str),
    Break,
    BlankLine,
}

enum BlockMultiline<'a> {
    List(Vec<&'a str>),
    Note(Vec<&'a str>),
    Table(Vec<&'a str>),
    Fragment { language: &'a str, content: Vec<&'a str> },
}

fn parse_embed(line: &str) -> Option<Block> {
    let line = line.trim();
    if let Some(stripped) = line.strip_prefix("!<") {
        if let Some(stripped) = stripped.strip_suffix(">") {
            if let Some((label, path)) = stripped.split_once("::") {
                let label = label.trim().to_string();
                let path = path.trim().to_string();
                return Some(Block::Embed { label, path })
            } else {
                let label = String::new();
                let path = stripped.trim().to_string();
                return Some(Block::Embed { label, path })
            }
        }
    }
    return None;
}

fn parse_math(line: &str) -> Option<Block> {
    let line = line.trim();
    if let Some(stripped) = line.strip_prefix("$$") {
        if let Some(stripped) = stripped.strip_suffix("$$") {
            return Some(Block::Math(stripped.trim().to_string()))
        }
    }
    return None;
}