use serde_yaml::Mapping;

use super::document::LinkType;
use crate::markdown::writer::MarkdownWriter;
use crate::model;
use crate::model::config::MarkdownOptions;
use crate::model::document::DocumentInlines;
use crate::model::node::ColumnAlignment;
use crate::model::reference::{Reference, ReferenceType};
use crate::model::{InlinesContext, Key, Lang, Level, LibraryUrl, Title};

pub type Blocks = Vec<GraphBlock>;
pub type GraphInlines = Vec<GraphInline>;

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

#[derive(Debug, Clone, PartialEq)]
pub enum GraphInline {
    Code(Option<Lang>, String),
    Emph(GraphInlines),
    Image(LibraryUrl, Title, GraphInlines),
    LineBreak,
    Link(LibraryUrl, Title, LinkType, GraphInlines),
    Reference(Reference),
    Math(String),
    RawInline(Lang, String),
    SmallCaps(GraphInlines),
    SoftBreak,
    Space,
    Str(String),
    Strikeout(GraphInlines),
    Strong(GraphInlines),
    Subscript(GraphInlines),
    Superscript(GraphInlines),
    Underline(GraphInlines),
}

impl From<&str> for GraphInline {
    fn from(s: &str) -> Self {
        GraphInline::Str(s.to_string())
    }
}

impl From<String> for GraphInline {
    fn from(s: String) -> Self {
        GraphInline::Str(s)
    }
}

#[allow(dead_code)]
impl GraphBlock {
    fn is_sparce_list(&self) -> bool {
        match self {
            GraphBlock::BulletList(items) => items
                .iter()
                .any(|item| item.iter().filter(|block| block.is_paragraph()).count() > 1),
            GraphBlock::OrderedList(items) => items
                .iter()
                .any(|item| item.iter().filter(|block| block.is_paragraph()).count() > 1),
            _ => false,
        }
    }

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

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

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

    pub fn to_markdown(&self, options: &MarkdownOptions) -> String {
        match self {
            GraphBlock::Frontmatter(mapping) => format!("---\n{}---\n", frontmatter_to_yaml(mapping)),
            GraphBlock::Plain(inlines) => format!("{}\n", inlines_to_markdown(inlines, options)),
            GraphBlock::Para(inlines) => format!("{}\n", inlines_to_markdown(inlines, options)),
            GraphBlock::LineBlock(lines) => lines
                .iter()
                .map(|line| inlines_to_markdown(line, options))
                .collect::<Vec<String>>()
                .join("\n"),
            GraphBlock::CodeBlock(lang, text) => {
                let fence = options
                    .formatting
                    .code_block_token()
                    .repeat(options.formatting.code_block_token_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)
                    })
            }
            GraphBlock::RawBlock(_, text) => text.clone(),
            GraphBlock::BlockQuote(blocks) => {
                blocks_to_markdown_sparce(blocks, options)
                    .lines()
                    .map(|line| format!("> {}", line))
                    .map(|line| line.trim().to_string())
                    .collect::<Vec<String>>()
                    .join("\n")
                    + "\n"
            }
            GraphBlock::OrderedList(items) => 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(item, self.is_sparce_list(), options),
                        num,
                        options.formatting.ordered_list_token_char(),
                    )
                })
                .collect::<Vec<String>>()
                .join(if self.is_sparce_list() { "\n" } else { "" }),
            GraphBlock::BulletList(items) => items
                .iter()
                .map(|item| {
                    left_pad_and_prefix(
                        &blocks_to_markdown_and(item, self.is_sparce_list(), options),
                        options.formatting.list_token(),
                    )
                })
                .collect::<Vec<String>>()
                .join(if self.is_sparce_list() { "\n" } else { "" }),
            GraphBlock::Header(level, inlines) => {
                format!(
                    "{} {}\n",
                    "#".repeat(*level as usize),
                    inlines_to_markdown(inlines, options)
                )
            }
            GraphBlock::HorizontalRule => {
                let fmt = &options.formatting;
                format!("{}\n", fmt.rule_token().repeat(fmt.rule_token_count()))
            }
            GraphBlock::Table(_, _, _) => {
                let writer = MarkdownWriter::new(options.clone());
                format!("{}\n", writer.write(vec![self.clone()]))
            }
        }
    }
}

impl GraphInline {
    pub fn from_string(str: &str) -> GraphInlines {
        vec![GraphInline::Str(str.to_string())]
    }
    pub fn to_markdown(&self, options: &MarkdownOptions) -> String {
        match self {
            GraphInline::Str(text) => text.clone(),
            GraphInline::Emph(emph) => {
                let t = options.formatting.emphasis_token();
                format!("{t}{}{t}", inlines_to_markdown(emph, options))
            }
            GraphInline::Underline(underline) => inlines_to_markdown(underline, options),
            GraphInline::Strong(strong) => {
                let t = options.formatting.strong_token();
                format!("{t}{}{t}", inlines_to_markdown(strong, options))
            }
            GraphInline::Strikeout(strikeout) => {
                format!("~~{}~~", inlines_to_markdown(strikeout, options))
            }
            GraphInline::Superscript(superscript) => {
                format!("^{}^", inlines_to_markdown(superscript, options))
            }
            GraphInline::Subscript(subscript) => {
                format!("~{}~", inlines_to_markdown(subscript, options))
            }
            GraphInline::SmallCaps(small_caps) => inlines_to_markdown(small_caps, options),
            GraphInline::Code(_, text) => format!("`{}`", text),
            GraphInline::Space => " ".into(),
            GraphInline::SoftBreak => "\n".into(),
            GraphInline::LineBreak => "\n".into(),
            GraphInline::Link(url, _, link_type, inlines) => {
                let text = inlines_to_markdown(inlines, options);
                if *link_type == LinkType::WikiLinkPiped {
                    return format!("[[{}|{}]]", url, text);
                }
                if *link_type == LinkType::WikiLink {
                    return format!("[[{}]]", url);
                }
                if model::is_ref_url(url) {
                    format!(
                        "[{}]({})",
                        text,
                        append_refs_extension(url, &options.refs_extension)
                    )
                } else if text.eq_ignore_ascii_case(url) {
                    format!("<{}>", url)
                } else {
                    format!("[{}]({})", text, url)
                }
            }
            GraphInline::Reference(reference) => {
                let url = reference.key.to_library_url();
                match reference.reference_type {
                    ReferenceType::WikiLinkPiped => {
                        format!("[[{}|{}]]", url, reference.text)
                    }
                    ReferenceType::WikiLink => format!("[[{}]]", url),
                    ReferenceType::Regular => {
                        format!(
                            "[{}]({})",
                            reference.text,
                            append_refs_extension(&url, &options.refs_extension)
                        )
                    }
                }
            }
            GraphInline::Image(url, _, inlines) => {
                format!("![{}]({})", inlines_to_markdown(inlines, options), url)
            }
            GraphInline::RawInline(_, content) => format!("`{}`", content),
            GraphInline::Math(math) => format!("${}$", math),
        }
    }
    pub fn plain_text(&self) -> String {
        match self {
            GraphInline::Str(text) => text.clone(),
            GraphInline::Emph(emph) => to_plain_text(emph),
            GraphInline::Underline(underline) => to_plain_text(underline),
            GraphInline::Strong(strong) => to_plain_text(strong),
            GraphInline::Strikeout(strikeout) => to_plain_text(strikeout),
            GraphInline::Superscript(superscript) => to_plain_text(superscript),
            GraphInline::Subscript(subscript) => to_plain_text(subscript),
            GraphInline::SmallCaps(small_caps) => to_plain_text(small_caps),
            GraphInline::Code(_, text) => text.clone(),
            GraphInline::Space => " ".into(),
            GraphInline::SoftBreak => "\n".into(),
            GraphInline::LineBreak => "\n".into(),
            GraphInline::Link(_, _, _, inlines) => to_plain_text(inlines),
            GraphInline::Reference(reference) => reference.text.clone(),
            GraphInline::Image(_, _, inlines) => to_plain_text(inlines),
            GraphInline::RawInline(_, content) => content.clone(),
            _ => "".into(),
        }
    }

    pub fn ref_keys(&self) -> Vec<Key> {
        match self {
            GraphInline::Emph(emph) => emph.iter().flat_map(|inline| inline.ref_keys()).collect(),
            GraphInline::Underline(underline) => underline
                .iter()
                .flat_map(|inline| inline.ref_keys())
                .collect(),
            GraphInline::Strong(strong) => {
                strong.iter().flat_map(|inline| inline.ref_keys()).collect()
            }
            GraphInline::Strikeout(strikeout) => strikeout
                .iter()
                .flat_map(|inline| inline.ref_keys())
                .collect(),
            GraphInline::Superscript(superscript) => superscript
                .iter()
                .flat_map(|inline| inline.ref_keys())
                .collect(),
            GraphInline::Subscript(subscript) => subscript
                .iter()
                .flat_map(|inline| inline.ref_keys())
                .collect(),
            GraphInline::SmallCaps(small_caps) => small_caps
                .iter()
                .flat_map(|inline| inline.ref_keys())
                .collect(),
            GraphInline::Link(_, _, _, inlines) => inlines
                .iter()
                .flat_map(|inline| inline.ref_keys())
                .collect(),
            GraphInline::Reference(reference) => vec![reference.key.clone()],
            GraphInline::Image(_, _, inlines) => inlines
                .iter()
                .flat_map(|inline| inline.ref_keys())
                .collect(),
            _ => vec![],
        }
    }

    pub fn normalize(&self, context: impl InlinesContext) -> GraphInline {
        match self {
            GraphInline::Emph(emph) => GraphInline::Emph(
                emph.iter()
                    .map(|inline| inline.normalize(context))
                    .collect(),
            ),

            GraphInline::Strong(emph) => GraphInline::Strong(
                emph.iter()
                    .map(|inline| inline.normalize(context))
                    .collect(),
            ),
            GraphInline::Underline(emph) => GraphInline::Underline(
                emph.iter()
                    .map(|inline| inline.normalize(context))
                    .collect(),
            ),

            GraphInline::Strikeout(emph) => GraphInline::Strikeout(
                emph.iter()
                    .map(|inline| inline.normalize(context))
                    .collect(),
            ),
            GraphInline::Superscript(emph) => GraphInline::Superscript(
                emph.iter()
                    .map(|inline| inline.normalize(context))
                    .collect(),
            ),
            GraphInline::Subscript(emph) => GraphInline::Subscript(
                emph.iter()
                    .map(|inline| inline.normalize(context))
                    .collect(),
            ),
            GraphInline::SmallCaps(emph) => GraphInline::SmallCaps(
                emph.iter()
                    .map(|inline| inline.normalize(context))
                    .collect(),
            ),
            GraphInline::Reference(reference) => {
                let new_text = match reference.reference_type {
                    ReferenceType::Regular => context
                        .get_ref_title(&reference.key)
                        .unwrap_or_else(|| reference.text.clone()),
                    ReferenceType::WikiLink => String::new(),
                    ReferenceType::WikiLinkPiped => reference.text.clone(),
                };

                GraphInline::Reference(Reference {
                    key: reference.key.clone(),
                    text: new_text,
                    reference_type: reference.reference_type,
                })
            }
            _ => self.clone(),
        }
    }

    pub fn change_key(&self, target_key: &Key, updated_key: &Key) -> GraphInline {
        match self {
            GraphInline::Emph(emph) => GraphInline::Emph(
                emph.iter()
                    .map(|inline| inline.change_key(target_key, updated_key))
                    .collect(),
            ),

            GraphInline::Strong(emph) => GraphInline::Strong(
                emph.iter()
                    .map(|inline| inline.change_key(target_key, updated_key))
                    .collect(),
            ),
            GraphInline::Underline(emph) => GraphInline::Underline(
                emph.iter()
                    .map(|inline| inline.change_key(target_key, updated_key))
                    .collect(),
            ),

            GraphInline::Strikeout(emph) => GraphInline::Strikeout(
                emph.iter()
                    .map(|inline| inline.change_key(target_key, updated_key))
                    .collect(),
            ),
            GraphInline::Superscript(emph) => GraphInline::Superscript(
                emph.iter()
                    .map(|inline| inline.change_key(target_key, updated_key))
                    .collect(),
            ),
            GraphInline::Subscript(emph) => GraphInline::Subscript(
                emph.iter()
                    .map(|inline| inline.change_key(target_key, updated_key))
                    .collect(),
            ),
            GraphInline::SmallCaps(emph) => GraphInline::SmallCaps(
                emph.iter()
                    .map(|inline| inline.change_key(target_key, updated_key))
                    .collect(),
            ),
            GraphInline::Reference(reference) => {
                if reference.key.eq(target_key) {
                    return GraphInline::Reference(Reference {
                        key: updated_key.clone(),
                        text: reference.text.clone(),
                        reference_type: reference.reference_type,
                    });
                }
                self.clone()
            }
            _ => self.clone(),
        }
    }

    pub fn is_ref(&self) -> bool {
        matches!(self, GraphInline::Reference(_))
    }
}

fn left_pad_and_prefix(text: &str, list_token: &str) -> String {
    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, line));
        } else {
            result.push_str(&format!("{} {}\n", " ".repeat(list_token.len()), line));
        }
    }

    result
}

fn left_pad_and_prefix_num(text: &str, num: usize, ordered_list_token: char) -> String {
    let prefix = format!("{}{}{}", num, ordered_list_token, if num > 9 { "" } else { " " });
    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, line));
        } else {
            result.push_str(&format!("{} {}\n", " ".repeat(prefix.len()), line));
        }
    }

    result
}

pub fn to_plain_text(content: &GraphInlines) -> String {
    content
        .iter()
        .map(|i| i.plain_text())
        .collect::<Vec<String>>()
        .join("")
}

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

pub fn inlines_to_markdown(content: &GraphInlines, options: &MarkdownOptions) -> String {
    content
        .iter()
        .map(|i| i.to_markdown(options))
        .collect::<Vec<String>>()
        .join("")
}

fn append_refs_extension(url: &str, extension: &str) -> String {
    let (path, fragment) = match url.split_once('#') {
        Some((p, f)) => (p, Some(f)),
        None => (url, None),
    };

    let new_path = if has_file_extension(path) {
        path.to_string()
    } else {
        format!("{path}{extension}")
    };

    match fragment {
        Some(f) => format!("{new_path}#{f}"),
        None => new_path,
    }
}

fn has_file_extension(path: &str) -> bool {
    std::path::Path::new(path)
        .extension()
        .and_then(|e| e.to_str())
        .map(|ext| ext.chars().any(|c| c.is_ascii_alphabetic()))
        .unwrap_or(false)
}

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 {
    ensure_trailing_newline(
        blocks
            .iter()
            .map(|block| block.to_markdown(options))
            .collect::<Vec<String>>()
            .join(if sparce { "\n" } else { "" }),
    )
}

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 {
    ensure_trailing_newline(
        blocks
            .iter()
            .map(|block| block.to_markdown(options))
            .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"),
    )
}

pub fn to_graph_inlines(content: &DocumentInlines, relative_to: &str) -> Vec<GraphInline> {
    content
        .iter()
        .map(|i| i.to_graph_inline(relative_to))
        .collect()
}

#[cfg(test)]
pub mod tests {
    use crate::model::config::MarkdownOptions;
    use crate::model::graph::blocks_to_markdown;
    use crate::model::graph::{GraphBlock, GraphInline};
    use indoc::indoc;

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

    #[test]
    fn test_ordered_list_to_markdown() {
        let list = vec![GraphBlock::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![GraphBlock::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![GraphBlock::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![GraphBlock::BulletList(vec![vec![
            plain("item1"),
            GraphBlock::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![GraphBlock::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![GraphBlock::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![GraphBlock::BulletList(vec![vec![
            plain("item1"),
            GraphBlock::BulletList(vec![vec![
                plain("item2"),
                GraphBlock::BulletList(vec![vec![plain("item3")]]),
            ]]),
        ]])];
        assert_eq!(
            indoc! {"
                - item1
                  - item2
                    - item3
                "},
            blocks_to_markdown(&list, &MarkdownOptions::default()),
        );
    }
}
