use std::{
    fmt::{self, Display, Write},
    iter,
};

const TICKS: &str = "```";
const NL_TICKS: &str = "\n```";

/// Wrap a [Display] in markdown code-block ticks ([TICKS])
pub fn to_custom_code_block(key: &str, content: impl Display) -> String {
    let mut out = String::new();
    write_custom_code_block(&mut out, key, content).unwrap();
    out
}

/// Wrap a [Display] in markdown code-block ticks ([TICKS])
pub fn write_custom_code_block(mut w: impl Write, key: &str, content: impl Display) -> fmt::Result {
    write!(w, "{TICKS}{key}\n{content}\n{TICKS}")
}

/// Try to unwrap a string from within markdown code-block ticks ([TICKS])
pub fn try_from_custom_code_block<'a>(key: &str, code_block: &'a str) -> Option<&'a str> {
    code_block
        .trim()
        .strip_prefix(TICKS)?
        .strip_prefix(key)?
        .strip_prefix("\n")?
        .strip_suffix(TICKS)?
        .strip_suffix("\n")
}

#[derive(Debug, Clone, Copy)]
pub enum MdItem<'a> {
    /// A line of regular markdown, but not a code block.
    Line(&'a str),

    /// A markdown code block
    CodeBlock {
        /// The key or language of the code block.
        key: &'a str,

        /// Everything in-between the ticks.
        content: &'a str,

        /// The entire code-block, including ticks.
        span: &'a str,
    },
}

/// Iterate over code-blocks in a markdown string
pub fn iter_lines_and_code_blocks(mut md: &str) -> impl Iterator<Item = MdItem<'_>> {
    iter::from_fn(move || {
        if md.is_empty() {
            return None;
        }

        if !md.starts_with(TICKS) {
            // line does not start with ticks, return a normal line.
            let line;
            if let Some(i) = md.find('\n') {
                let i = i + 1;
                line = &md[..i];
                md = &md[i..];
            } else {
                line = md;
                md = "";
            }
            return Some(MdItem::Line(line));
        }

        let mut i = TICKS.len();
        let from_key = &md[i..];

        let Some((key, from_content)) = from_key.split_once('\n') else {
            // no more newlines, return the remaining string as the final line.
            let rest = md;
            md = "";
            return Some(MdItem::Line(rest));
        };
        i += key.len() + "\n".len();

        let Some(end) = from_content.find(NL_TICKS) else {
            // no closing ticks, return a line instead.
            let line;
            if let Some(i) = md.find('\n') {
                let i = i + 1;
                line = &md[..i];
                md = &md[i..];
            } else {
                line = md;
                md = "";
            }
            return Some(MdItem::Line(line));
        };
        let content = &from_content[..end];
        i += end + NL_TICKS.len();

        if md[i..].starts_with("\n") {
            i += 1;
        };
        let span = &md[..i];
        md = &md[i..];

        Some(MdItem::CodeBlock { key, content, span })
    })
}

#[cfg(test)]
mod test {
    use super::iter_lines_and_code_blocks;

    #[test]
    fn iter_markdown() {
        let markdown = r#"
# Hello world
## Subheader
- 1
```foo
  whatever
     some code
 Hi mom!
``` 

```` # wrong number of ticks, but that's ok
 ``` # indented ticks
```

``` # no closing ticks
            "#;

        let list: Vec<_> = iter_lines_and_code_blocks(markdown).collect();
        insta::assert_snapshot!(markdown);
        insta::assert_debug_snapshot!(list);
    }
}
