Documentation
use pulldown_cmark::{Event, HeadingLevel, MetadataBlockKind, Tag, TagEnd};
use pulldown_cmark_to_cmark::cmark_with_options;

use crate::model::config::MarkdownOptions;
use crate::model::inline::{inlines_to_markdown, Inline, Inlines};
use crate::model::node::ColumnAlignment;
use crate::model::writer::{frontmatter_to_yaml, Block};
use crate::model::{document, is_ref_url};

pub struct MarkdownWriter {
    options: MarkdownOptions,
}

impl MarkdownWriter {
    pub fn new(options: MarkdownOptions) -> MarkdownWriter {
        MarkdownWriter { options }
    }

    pub fn write(&self, blocks: Vec<Block>) -> String {
        let mut buf = String::new();
        cmark_with_options(
            self.blocks_events(blocks).iter().cloned(),
            &mut buf,
            self.options.formatting.to_cmark_options(),
        )
        .unwrap();
        buf
    }

    fn blocks_events(&self, iter: Vec<Block>) -> Vec<Event<'_>> {
        iter.into_iter()
            .flat_map(|block| self.block_events(block))
            .collect()
    }

    fn block_events(&self, block: Block) -> Vec<Event<'_>> {
        let mut events = Vec::new();
        match block {
            Block::Frontmatter(mapping) => {
                events.push(Event::Start(Tag::MetadataBlock(
                    MetadataBlockKind::YamlStyle,
                )));
                events.push(Event::Text(frontmatter_to_yaml(&mapping).into()));
                events.push(Event::End(TagEnd::MetadataBlock(
                    MetadataBlockKind::YamlStyle,
                )));
            }
            Block::Header(level, inlines) => {
                events.push(Event::Start(Tag::Heading {
                    level: header_level(level),
                    id: None,
                    classes: vec![],
                    attrs: vec![],
                }));
                events.append(&mut self.inlines_to_events(inlines));
                events.push(Event::End(TagEnd::Heading(header_level(level))));
            }
            Block::BlockQuote(blocks) => {
                events.push(Event::Start(Tag::BlockQuote(None)));
                events.append(&mut self.blocks_events(blocks));
                events.push(Event::End(TagEnd::BlockQuote(None)));
            }
            Block::BulletList(items) => {
                events.push(Event::Start(Tag::List(None)));
                for item in items {
                    events.push(Event::Start(Tag::Item));
                    events.append(&mut self.blocks_events(item));
                    events.push(Event::End(TagEnd::Item));
                }
                events.push(Event::End(TagEnd::List(false)));
            }
            Block::OrderedList(items) => {
                events.push(Event::Start(Tag::List(Some(1))));
                for blocks in items {
                    events.push(Event::Start(Tag::Item));
                    events.append(&mut self.blocks_events(blocks));
                    events.push(Event::End(TagEnd::Item));
                }
                events.push(Event::End(TagEnd::List(true)));
            }
            Block::Para(inlines) => {
                events.push(Event::Start(Tag::Paragraph));
                events.append(&mut self.inlines_to_events(inlines));
                events.push(Event::End(TagEnd::Paragraph));
            }
            Block::RawBlock(_, content) => {
                events.push(Event::Html(content.into()));
            }
            Block::HorizontalRule => {
                events.push(Event::Rule);
            }
            Block::Plain(inlines) => {
                events.push(Event::Start(Tag::Paragraph));
                events.append(&mut self.inlines_to_events(inlines));
                events.push(Event::End(TagEnd::Paragraph));
            }
            Block::LineBlock(lines) => {
                events.push(Event::Start(Tag::Paragraph));
                lines.iter().for_each(|line| {
                    events.append(&mut self.inlines_to_events(line.clone()));
                    events.push(Event::HardBreak);
                });
                events.push(Event::End(TagEnd::Paragraph));
            }
            Block::CodeBlock(_, content) => {
                events.push(Event::Start(Tag::CodeBlock(
                    pulldown_cmark::CodeBlockKind::Fenced(content.into()),
                )));
                events.push(Event::End(TagEnd::CodeBlock));
            }
            Block::Table(header_row, alignment, rows) => {
                let table_md = self.render_aligned_table(&header_row, &alignment, &rows);
                events.push(Event::Html(table_md.into()));
            }
        }
        events
    }

    fn inlines_to_events(&self, inlines: Inlines) -> Vec<Event<'_>> {
        let mut events = Vec::new();
        for inline in inlines {
            match inline {
                Inline::Code(_, code) => {
                    events.push(Event::Start(Tag::CodeBlock(
                        pulldown_cmark::CodeBlockKind::Fenced(code.into()),
                    )));
                    events.push(Event::End(TagEnd::CodeBlock));
                }
                Inline::Emph(vec) => {
                    events.push(Event::Start(Tag::Emphasis));
                    events.extend(self.inlines_to_events(vec));
                    events.push(Event::End(TagEnd::Emphasis));
                }
                Inline::Image(url, title, _) => {
                    events.push(Event::Start(Tag::Image {
                        title: title.into(),
                        link_type: pulldown_cmark::LinkType::Autolink,
                        dest_url: url.into(),
                        id: "".into(),
                    }));
                    events.push(Event::End(TagEnd::Image));
                }
                Inline::LineBreak => {
                    events.push(Event::HardBreak);
                }
                Inline::Link(url, title, t, inlines) => {
                    let text = inlines_to_markdown(&inlines, &self.options);
                    if !is_ref_url(&url) && text.eq_ignore_ascii_case(&url) {
                        events.push(Event::Start(Tag::Link {
                            title: title.into(),
                            link_type: pulldown_cmark::LinkType::Autolink,
                            dest_url: url.into(),
                            id: "".into(),
                        }));
                        events.extend(self.inlines_to_events(inlines));
                        events.push(Event::End(TagEnd::Link));
                    } else {
                        events.push(Event::Start(Tag::Link {
                            title: title.into(),
                            link_type: link_type(t),
                            dest_url: url.into(),
                            id: "".into(),
                        }));
                        events.extend(self.inlines_to_events(inlines));
                        events.push(Event::End(TagEnd::Link));
                    }
                }
                Inline::Reference(reference) => {
                    events.push(Event::Start(Tag::Link {
                        title: "".into(),
                        link_type: link_type(reference.reference_type.to_link_type()),
                        dest_url: reference.key.to_library_url().into(),
                        id: "".into(),
                    }));
                    events.push(Event::Text(reference.text.into()));
                    events.push(Event::End(TagEnd::Link));
                }
                Inline::Math(math) => {
                    events.push(Event::Html(format!("\\({}\\)", math).into()));
                }
                Inline::RawInline(_, content) => {
                    events.push(Event::Html(content.into()));
                }
                Inline::SmallCaps(vec) => {
                    events.extend(self.inlines_to_events(vec));
                }
                Inline::SoftBreak => {
                    events.push(Event::SoftBreak);
                }
                Inline::Space => {
                    events.push(Event::Text(" ".into()));
                }
                Inline::Str(text) => {
                    events.push(Event::Text(text.into()));
                }
                Inline::Strikeout(vec) => {
                    events.push(Event::Start(Tag::Strikethrough));
                    events.extend(self.inlines_to_events(vec));
                    events.push(Event::End(TagEnd::Strikethrough));
                }
                Inline::Strong(vec) => {
                    events.push(Event::Start(Tag::Strong));
                    events.extend(self.inlines_to_events(vec));
                    events.push(Event::End(TagEnd::Strong));
                }
                Inline::Subscript(vec) => {
                    events.extend(self.inlines_to_events(vec));
                }
                Inline::Superscript(vec) => {
                    events.extend(self.inlines_to_events(vec));
                }
                Inline::Underline(vec) => {
                    events.extend(self.inlines_to_events(vec));
                }
            }
        }
        events
    }

    fn render_aligned_table(
        &self,
        header: &[Inlines],
        alignment: &[ColumnAlignment],
        rows: &[Vec<Inlines>],
    ) -> String {
        let header_strs: Vec<String> = header
            .iter()
            .map(|c| inlines_to_markdown(c, &self.options).replace('|', "\\|"))
            .collect();
        let row_strs: Vec<Vec<String>> = rows
            .iter()
            .map(|row| {
                row.iter()
                    .map(|c| inlines_to_markdown(c, &self.options).replace('|', "\\|"))
                    .collect()
            })
            .collect();

        let num_cols = header.len();
        let mut widths: Vec<usize> = header_strs.iter().map(|s| s.chars().count()).collect();

        for row in &row_strs {
            for (i, cell) in row.iter().enumerate() {
                if i < widths.len() {
                    widths[i] = widths[i].max(cell.chars().count());
                }
            }
        }

        for w in &mut widths {
            if *w < 3 {
                *w = 3;
            }
        }

        let mut result = String::new();

        result.push('|');
        for (i, cell) in header_strs.iter().enumerate() {
            let width = widths.get(i).copied().unwrap_or(3);
            result.push(' ');
            result.push_str(&pad_cell(cell, width, alignment.get(i).copied()));
            result.push_str(" |");
        }
        result.push('\n');

        result.push('|');
        for (i, &width) in widths.iter().enumerate() {
            let al = alignment.get(i).copied().unwrap_or(ColumnAlignment::None);
            result.push_str(&separator_cell(width, al));
            result.push('|');
        }
        result.push('\n');

        for row in &row_strs {
            result.push('|');
            for i in 0..num_cols {
                let cell = row.get(i).map(|s| s.as_str()).unwrap_or("");
                let width = widths.get(i).copied().unwrap_or(3);
                result.push(' ');
                result.push_str(&pad_cell(cell, width, alignment.get(i).copied()));
                result.push_str(" |");
            }
            result.push('\n');
        }

        result
    }
}

fn pad_cell(content: &str, width: usize, alignment: Option<ColumnAlignment>) -> String {
    let content_len = content.chars().count();
    if content_len >= width {
        return content.to_string();
    }
    let padding = width - content_len;
    match alignment.unwrap_or(ColumnAlignment::None) {
        ColumnAlignment::Right => format!("{}{}", " ".repeat(padding), content),
        ColumnAlignment::Center => {
            let left = padding / 2;
            let right = padding - left;
            format!("{}{}{}", " ".repeat(left), content, " ".repeat(right))
        }
        _ => format!("{}{}", content, " ".repeat(padding)),
    }
}

fn separator_cell(width: usize, alignment: ColumnAlignment) -> String {
    match alignment {
        ColumnAlignment::Left => format!(":{}", "-".repeat(width + 1)),
        ColumnAlignment::Right => format!("{}:", "-".repeat(width + 1)),
        ColumnAlignment::Center => format!(":{}:", "-".repeat(width)),
        ColumnAlignment::None => format!(" {} ", "-".repeat(width)),
    }
}

fn link_type(link_type: document::LinkType) -> pulldown_cmark::LinkType {
    match link_type {
        document::LinkType::Markdown => pulldown_cmark::LinkType::Inline,
        document::LinkType::WikiLink => pulldown_cmark::LinkType::WikiLink { has_pothole: false },
        document::LinkType::WikiLinkPiped => {
            pulldown_cmark::LinkType::WikiLink { has_pothole: true }
        }
    }
}

fn header_level(level: u8) -> HeadingLevel {
    match level {
        1 => HeadingLevel::H1,
        2 => HeadingLevel::H2,
        3 => HeadingLevel::H3,
        4 => HeadingLevel::H4,
        5 => HeadingLevel::H5,
        6 => HeadingLevel::H6,
        _ => HeadingLevel::H6,
    }
}