use std::iter::{self, once};

use crate::markdown::Style;

use super::{Item, Span, Token, TokenKind, tokenize};

pub fn parse(text: &str) -> Vec<Item<'_>> {
    let tokens: Vec<_> = tokenize(text).collect();
    parse_tokens(&tokens)
}

pub fn parse_tokens<'a>(mut tokens: &[Token<'a>]) -> Vec<Item<'a>> {
    // pretend that the first token was preceeded by a newline.
    // means we don't have to handle the first token as a special case.
    let mut prev = TokenKind::Newline;

    let mut style = Style::default();

    iter::from_fn(move || {
        if tokens.is_empty() {
            return None;
        }

        let token = tokens.first().unwrap();
        tokens = &tokens[1..];

        let start_of_line = prev == TokenKind::Newline;
        prev = token.kind;

        let mut basic_style: Option<fn(&mut Style) -> &mut bool> = None;

        match token.kind {
            TokenKind::CodeBlock if start_of_line => {
                let language = collect_until(
                    None,
                    &mut tokens,
                    any_of([TokenKind::Newline]),
                );

                let code = collect_until(
                    None,
                    &mut tokens,
                    series([TokenKind::Newline, TokenKind::CodeBlock]),
                );

                let all = [
                    &token.span,
                    &language,
                    &code,
                ].into_iter().fold(Span::empty(), |a, b| a.try_merge(b).unwrap());

                let language = language.trim_end_matches("\n");
                let code = code.trim_end_matches("\n```");

                return Some(Item::CodeBlock { all, language, code });
            }

            TokenKind::Newline => style = Style::default(),

            TokenKind::Strong => basic_style = Some(|s| &mut s.strong),
            TokenKind::Italic => basic_style = Some(|s| &mut s.italics),
            TokenKind::Strikethrough => basic_style = Some(|s| &mut s.strikethrough),

            TokenKind::CodeBlock | TokenKind::Mono => {
                let span = collect_until(
                    Some(token),
                    &mut tokens,
                    any_of([TokenKind::Mono, TokenKind::CodeBlock, TokenKind::Newline]),
                );

                let mut style = style;
                style.code = true;

                return Some(Item::Text { span, style });
            }

            // TODO: different heading strengths
            TokenKind::Heading(h) if start_of_line => style.heading = Some(h),
            TokenKind::Quote if start_of_line => style.quoted = true,

            // TODO: replace dashes with dots
            //// TODO: indented list entries
            //TokenKind::ListEntry if start_of_line => {
            //    job.append("• ", 0.0, format_from_style(egui_style, &style));
            //    continue;
            //}

            TokenKind::Text
            // the following tokens are only richly rendered if encountered e.g. at start_of_line.
            | TokenKind::Indentation
            | TokenKind::ListEntry
            | TokenKind::Heading(..)
            | TokenKind::Quote => {}
        }

        // if we encountered a marker for Bold, Italic, or Strikethrough, toggle that style and
        // render the token with the style enabled.
        if let Some(basic_style) = basic_style {
            let mut tmp_style = style;
            *basic_style(&mut tmp_style) = true;
            *basic_style(&mut style) ^= true; // toggle
            return Some(Item::Text {
                span: token.span.clone(),
                style: tmp_style,
            });
        }

        Some(Item::Text {
            span: token.span.clone(),
            style,
        })
    })
    .collect()
}

fn series<'a, const N: usize>(of: [TokenKind; N]) -> impl FnMut(&[Token<'a>; N]) -> bool {
    move |token| {
        of.iter()
            .zip(token)
            .all(|(kind, token)| kind == &token.kind)
    }
}

fn any_of<'a, const N: usize>(these: [TokenKind; N]) -> impl FnMut(&[Token<'a>; 1]) -> bool {
    move |[token]| these.contains(&token.kind)
}

/// Collect all tokens up to and including `pattern`, and merge them into a signle span.
///
/// `N` determines how many specific and consecutive tokens we are looking for.
/// i.e. if we were looking for a [TokenKind::Newline] followed by a [TokenKind::Quote], `N`
/// would equal `2`.
///
/// `pattern` is a function that accepts an array of `N` tokens and returns `true` if they match,
/// i.e. if we should stop collecting. [any_of] and [series] can help to construct this function.
///
/// The collected tokens will be split off the head of the slice referred to by `tokens`.
///
/// # Panic
/// Panics if `tokens` does not contain only consecutive adjacent spans.
fn collect_until<'a, const N: usize>(
    first_token: Option<&Token<'a>>,
    tokens: &mut &[Token<'a>],
    pattern: impl FnMut(&[Token<'a>; N]) -> bool,
) -> Span<'a>
where
    // &[T; N]: TryFrom<&[T]>
    for<'b> &'b [Token<'a>; N]: TryFrom<&'b [Token<'a>]>,
{
    let mut windows = tokens.windows(N).map(|slice| {
        <&[Token<'a>; N]>::try_from(slice)
            .ok()
            .expect("`windows` promises to return slices of length N")
    });

    let split_at = match windows.position(pattern) {
        Some(i) => i + N,
        None => tokens.len(), // consume everything
    };

    let (consume, keep) = tokens.split_at(split_at);
    *tokens = keep;

    once(first_token)
        .flatten()
        .chain(consume)
        .fold(Span::empty(), |span: Span<'_>, token| {
            span.try_merge(&token.span).unwrap()
        })
}
