use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::character::complete::alphanumeric1;
use nom::character::complete::line_ending;
use nom::character::complete::space1;
use nom::combinator::map;
use nom::combinator::not;
use nom::combinator::recognize;
use nom::multi::many_till;

use super::bold::bold;
use super::error::Res;
use super::link::link;
use super::parser_with_context::parser_with_context;
use super::token::BlankLine;
use super::token::LineBreak;
use super::token::Space;
use super::token::Span;
use super::token::Symbol;
use super::token::TextElement;
use super::Context;

pub fn line_break(input: &str) -> Res<&str, LineBreak> {
    map(line_ending, |s: &str| LineBreak { source: s })(input)
}

pub fn space(input: &str) -> Res<&str, Space> {
    map(space1, |s: &str| Space { source: s })(input)
}

fn span(input: &str) -> Res<&str, Span> {
    map(alphanumeric1, |s: &str| Span { source: s })(input)
}

pub fn symbol(symbol_tag: &'static str) -> impl for<'a> Fn(&'a str) -> Res<&'a str, Symbol<'a>> {
    move |i: &str| map(tag(symbol_tag), |s: &str| Symbol { source: s })(i)
}

/// A line containing only whitespace and then a line break
///
/// It is up to the caller to ensure this is called at the start of a line.
pub fn blank_line(input: &str) -> Res<&str, BlankLine> {
    map(
        recognize(many_till(
            map(space, TextElement::Space),
            map(line_break, TextElement::LineBreak),
        )),
        |contents| BlankLine { source: contents },
    )(input)
}

pub fn text_element<'r, 's>(context: Context<'r, 's>, i: &'s str) -> Res<&'s str, TextElement<'s>> {
    not(|i| context.check_exit_matcher(i))(i)?;

    let bold_matcher = parser_with_context!(bold)(&context);
    let link_matcher = parser_with_context!(link)(&context);

    alt((
        map(bold_matcher, TextElement::Bold),
        map(link_matcher, TextElement::Link),
        map(span, TextElement::Span),
        map(symbol("*"), TextElement::Symbol),
        map(symbol("["), TextElement::Symbol),
        map(symbol("]"), TextElement::Symbol),
        map(space, TextElement::Space),
        map(line_break, TextElement::LineBreak),
    ))(i)
}