use serde_yaml::Mapping;

use crate::markdown::writer::CmarkTableWriter;
use crate::model::config::{FormattingOptions, MarkdownOptions};
use crate::model::node::ColumnAlignment;
use crate::model::{Lang, Level};

use super::inline::{inlines_to_markdown, wrap_inlines, Inlines};

pub type Blocks = Vec<Block>;

#[derive(Debug, Clone, PartialEq)]
pub enum Block {
    Frontmatter(Mapping),
    Plain(Inlines),
    Para(Inlines),
    LineBlock(Vec<Inlines>),
    CodeBlock(Option<Lang>, String),
    RawBlock(String, String),
    BlockQuote(Blocks),
    OrderedList(Vec<Blocks>),
    BulletList(Vec<Blocks>),
    Header(Level, Inlines),
    HorizontalRule,
    Table(Vec<Inlines>, Vec<ColumnAlignment>, Vec<Vec<Inlines>>),
}

#[allow(dead_code)]
impl Block {
    fn is_sparce_list(&self) -> bool {
        match self {
            Block::BulletList(items) => items.iter().any(item_requires_blank_lines),
            Block::OrderedList(items) => items.iter().any(item_requires_blank_lines),
            _ => false,
        }
    }

    fn is_list(&self) -> bool {
        matches!(self, Block::BulletList(_) | Block::OrderedList(_))
    }

    fn is_paragraph(&self) -> bool {
        matches!(self, Block::Plain(_) | Block::Para(_))
    }

    fn requires_blank_line_separation(&self) -> bool {
        matches!(
            self,
            Block::CodeBlock(_, _)
                | Block::Table(_, _, _)
                | Block::BlockQuote(_)
                | Block::HorizontalRule
        )
    }

    fn is_frontmatter(&self) -> bool {
        matches!(self, Block::Frontmatter(_))
    }

    pub fn to_markdown(&self, options: &MarkdownOptions) -> String {
        self.to_markdown_indented(options, 0)
    }

    pub fn to_markdown_indented(&self, options: &MarkdownOptions, indent: usize) -> String {
        match self {
            Block::Frontmatter(mapping) => {
                format!("---\n{}---\n", frontmatter_to_yaml(mapping))
            }
            Block::Plain(inlines) => format!("{}\n", wrap_inlines(inlines, options, indent)),
            Block::Para(inlines) => format!("{}\n", wrap_inlines(inlines, options, indent)),
            Block::LineBlock(lines) => lines
                .iter()
                .map(|line| inlines_to_markdown(line, options))
                .collect::<Vec<String>>()
                .join("\n"),
            Block::CodeBlock(lang, text) => {
                let token = options.formatting.code_block_token_char();
                let mut max_run = 0;
                let mut run = 0;
                for ch in text.chars() {
                    if ch == token {
                        run += 1;
                        max_run = max_run.max(run);
                    } else {
                        run = 0;
                    }
                }
                let count = options.formatting.code_block_token_count().max(max_run + 1);
                let fence = token.to_string().repeat(count);
                lang.clone()
                    .filter(|lang| !lang.trim().is_empty())
                    .map(|lang| {
                        format!(
                            "{} {}\n{}\n{}\n",
                            fence,
                            lang,
                            text.trim_matches('\n'),
                            fence
                        )
                    })
                    .unwrap_or_else(|| {
                        format!("{}\n{}\n{}\n", fence, text.trim_matches('\n'), fence)
                    })
            }
            Block::RawBlock(_, text) => text.clone(),
            Block::BlockQuote(blocks) => {
                blocks_to_markdown_sparce_indented(blocks, options, indent + 2)
                    .lines()
                    .map(|line| format!("> {}", line))
                    .map(|line| line.trim().to_string())
                    .collect::<Vec<String>>()
                    .join("\n")
                    + "\n"
            }
            Block::OrderedList(items) => {
                let content_indent = options
                    .formatting
                    .ordered_list_content_indent()
                    .unwrap_or(0);
                let child_indent = indent
                    + ordered_prefix_indent(items.len(), &options.formatting).max(content_indent);
                items
                    .iter()
                    .enumerate()
                    .map(|(n, item)| {
                        let num = if options.formatting.increment_ordered_list_bullets() {
                            n + 1
                        } else {
                            1
                        };
                        left_pad_and_prefix_num(
                            &blocks_to_markdown_and_indented(
                                item,
                                self.is_sparce_list(),
                                options,
                                child_indent,
                            ),
                            num,
                            options.formatting.ordered_list_token_char(),
                            content_indent,
                        )
                    })
                    .collect::<Vec<String>>()
                    .join(if self.is_sparce_list() { "\n" } else { "" })
            }
            Block::BulletList(items) => {
                let content_indent = options.formatting.bullet_list_content_indent().unwrap_or(0);
                let child_indent = indent
                    + (options.formatting.list_token().chars().count() + 1).max(content_indent);
                items
                    .iter()
                    .map(|item| {
                        left_pad_and_prefix(
                            &blocks_to_markdown_and_indented(
                                item,
                                self.is_sparce_list(),
                                options,
                                child_indent,
                            ),
                            options.formatting.list_token(),
                            content_indent,
                        )
                    })
                    .collect::<Vec<String>>()
                    .join(if self.is_sparce_list() { "\n" } else { "" })
            }
            Block::Header(level, inlines) => {
                format!(
                    "{} {}\n",
                    "#".repeat(*level as usize),
                    inlines_to_markdown(inlines, options)
                )
            }
            Block::HorizontalRule => {
                let fmt = &options.formatting;
                format!("{}\n", fmt.rule_token().repeat(fmt.rule_token_count()))
            }
            Block::Table(_, _, _) => {
                let writer = CmarkTableWriter::new(options.clone());
                writer.write(vec![self.clone()])
            }
        }
    }
}

fn ordered_prefix_indent(item_count: usize, formatting: &FormattingOptions) -> usize {
    let last = if formatting.increment_ordered_list_bullets() {
        item_count.max(1)
    } else {
        1
    };
    let prefix = format!("{}{}", last, formatting.ordered_list_token_char());
    prefix.len() + 1
}

fn left_pad_and_prefix(text: &str, list_token: &str, content_indent: usize) -> String {
    let token_width = list_token.chars().count();
    let pad = content_indent.max(token_width + 1);
    let mut result = String::new();
    for (n, line) in text.lines().enumerate() {
        if line.is_empty() {
            result.push('\n');
        } else if n == 0 {
            result.push_str(&format!(
                "{}{}{}\n",
                list_token,
                " ".repeat(pad - token_width),
                line
            ));
        } else {
            result.push_str(&format!("{}{}\n", " ".repeat(pad), line));
        }
    }

    result
}

fn left_pad_and_prefix_num(
    text: &str,
    num: usize,
    ordered_list_token: char,
    content_indent: usize,
) -> String {
    let prefix = format!("{}{}", num, ordered_list_token);
    let prefix_width = prefix.chars().count();
    let pad = content_indent.max(prefix_width + 1);
    let mut result = String::new();
    for (n, line) in text.lines().enumerate() {
        if line.is_empty() {
            result.push('\n');
        } else if n == 0 {
            result.push_str(&format!(
                "{}{}{}\n",
                prefix,
                " ".repeat(pad - prefix_width),
                line
            ));
        } else {
            result.push_str(&format!("{}{}\n", " ".repeat(pad), line));
        }
    }

    result
}

pub fn frontmatter_to_yaml(mapping: &Mapping) -> String {
    if mapping.is_empty() {
        return "{}\n".to_string();
    }
    serde_yaml::to_string(mapping).unwrap_or_default()
}

fn ensure_trailing_newline(s: String) -> String {
    if s.is_empty() || s.ends_with('\n') {
        s
    } else {
        s + "\n"
    }
}

pub fn blocks_to_markdown_and(blocks: &Blocks, sparce: bool, options: &MarkdownOptions) -> String {
    blocks_to_markdown_and_indented(blocks, sparce, options, 0)
}

fn item_requires_blank_lines(item: &Blocks) -> bool {
    item.iter().filter(|block| block.is_paragraph()).count() > 1
        || item.windows(2).any(|pair| {
            pair[0].requires_blank_line_separation() || pair[1].requires_blank_line_separation()
        })
}

pub fn blocks_to_markdown_and_indented(
    blocks: &Blocks,
    sparce: bool,
    options: &MarkdownOptions,
    indent: usize,
) -> String {
    let rendered = blocks
        .iter()
        .map(|block| block.to_markdown_indented(options, indent))
        .collect::<Vec<String>>();

    let mut result = String::new();
    for (i, text) in rendered.iter().enumerate() {
        if i > 0
            && (sparce
                || blocks[i - 1].requires_blank_line_separation()
                || blocks[i].requires_blank_line_separation())
        {
            result.push('\n');
        }
        result.push_str(text);
    }

    ensure_trailing_newline(result)
}

pub fn blocks_to_markdown(blocks: &Blocks, options: &MarkdownOptions) -> String {
    ensure_trailing_newline(
        blocks
            .iter()
            .map(|block| block.to_markdown(options))
            .collect::<Vec<String>>()
            .join(""),
    )
}

pub fn blocks_to_markdown_sparce(blocks: &Blocks, options: &MarkdownOptions) -> String {
    blocks_to_markdown_sparce_indented(blocks, options, 0)
}

pub fn blocks_to_markdown_sparce_indented(
    blocks: &Blocks,
    options: &MarkdownOptions,
    indent: usize,
) -> String {
    ensure_trailing_newline(
        blocks
            .iter()
            .map(|block| block.to_markdown_indented(options, indent))
            .collect::<Vec<String>>()
            .join("\n"),
    )
}

pub fn blocks_to_markdown_sparce_skip_frontmatter(
    blocks: &Blocks,
    options: &MarkdownOptions,
) -> String {
    ensure_trailing_newline(
        blocks
            .iter()
            .filter_map(|block| (!block.is_frontmatter()).then_some(block.to_markdown(options)))
            .collect::<Vec<String>>()
            .join("\n"),
    )
}

#[cfg(test)]
pub mod tests {
    use crate::model::config::MarkdownOptions;
    use crate::model::inline::Inline;
    use crate::model::writer::{blocks_to_markdown, Block};
    use indoc::indoc;

    fn plain(text: &str) -> Block {
        Block::Plain(vec![Inline::Str(text.into())])
    }

    #[test]
    fn test_ordered_list_to_markdown() {
        let list = vec![Block::OrderedList(vec![
            vec![plain("item")],
            vec![plain("item")],
            vec![plain("item")],
            vec![plain("item")],
            vec![plain("item")],
            vec![plain("item")],
            vec![plain("item")],
            vec![plain("item")],
            vec![plain("item")],
            vec![plain("item")],
        ])];
        assert_eq!(
            indoc! {"
                1. item
                2. item
                3. item
                4. item
                5. item
                6. item
                7. item
                8. item
                9. item
                10. item
                "},
            blocks_to_markdown(&list, &MarkdownOptions::default()),
        );
    }

    #[test]
    fn test_ordered_list_with_para() {
        let list = vec![Block::OrderedList(vec![vec![
            plain("item1"),
            plain("para"),
        ]])];
        assert_eq!(
            indoc! {"
                1. item1

                   para
                "},
            blocks_to_markdown(&list, &MarkdownOptions::default()),
        );
    }

    #[test]
    fn test_list_to_markdown() {
        let list = vec![Block::BulletList(vec![
            vec![plain("item1")],
            vec![plain("item2")],
        ])];
        assert_eq!(
            indoc! {"
                - item1
                - item2
                "},
            blocks_to_markdown(&list, &MarkdownOptions::default()),
        );
    }

    #[test]
    fn test_sub_list() {
        let list = vec![Block::BulletList(vec![vec![
            plain("item1"),
            Block::BulletList(vec![vec![plain("item2")]]),
        ]])];
        assert_eq!(
            indoc! {"
                - item1
                  - item2
                "},
            blocks_to_markdown(&list, &MarkdownOptions::default()),
        );
    }

    #[test]
    fn test_list_with_para() {
        let list = vec![Block::BulletList(vec![vec![plain("item1"), plain("para")]])];
        assert_eq!(
            indoc! {"
                - item1

                  para
                "},
            blocks_to_markdown(&list, &MarkdownOptions::default()),
        );
    }

    #[test]
    fn test_list_with_para2() {
        let list = vec![Block::BulletList(vec![
            vec![plain("item1"), plain("para1")],
            vec![plain("item2"), plain("para2")],
        ])];
        assert_eq!(
            indoc! {"
                - item1

                  para1

                - item2

                  para2
                "},
            blocks_to_markdown(&list, &MarkdownOptions::default()),
        );
    }

    #[test]
    fn test_sub_sub_list() {
        let list = vec![Block::BulletList(vec![vec![
            plain("item1"),
            Block::BulletList(vec![vec![
                plain("item2"),
                Block::BulletList(vec![vec![plain("item3")]]),
            ]]),
        ]])];
        assert_eq!(
            indoc! {"
                - item1
                  - item2
                    - item3
                "},
            blocks_to_markdown(&list, &MarkdownOptions::default()),
        );
    }
}
