use crate::{IRes, LocatedParseError, Span, shex_parser_error::ParseError as ShExParseError};
use nom::{
    Err, Parser,
    branch::alt,
    bytes::complete::{is_not, tag, tag_no_case, take_until},
    character::complete::multispace1,
    combinator::value,
    multi::many0,
    sequence::{delimited, pair},
};
use std::fmt::Debug;

// Create a [`Span`][nom_locate::LocatedSpan] over the input.
/* fn span_from_str(input: &str) -> Span<'_> {
    Span::new(input)
}*/

// The result of a parse
// type ParseResult<'a, T> = Result<T, LocatedParseError>;

/// A combinator that modifies the associated error.
pub(crate) fn map_error<'a, T: 'a>(
    mut parser: impl Parser<Span<'a>, Output = T, Error = LocatedParseError> + 'a,
    mut error: impl FnMut() -> ShExParseError + 'a,
) -> impl FnMut(Span<'a>) -> IRes<'a, T> + 'a {
    move |input| {
        parser.parse(input).map_err(|e| match e {
            Err::Incomplete(_) => e,
            Err::Error(context) => {
                let mut err = error().at(input);
                err.append(context);
                Err::Error(err)
            },
            Err::Failure(context) => {
                let mut err = error().at(input);
                err.append(context);
                Err::Failure(err)
            },
        })
    }
}

/// A combinator to add tracing to the parser.
/// [fun] is an identifier for the parser and [parser] is the actual parser.
#[inline(always)]
#[allow(dead_code)] // The dead_code warning is disabled because this combinator is only used in debug mode.
#[allow(unused_variables)] // The unused_variables warning is disabled in case the tracing code commented out.
pub(crate) fn traced<'a, T, P>(fun: &'static str, mut parser: P) -> impl FnMut(Span<'a>) -> IRes<'a, T>
where
    T: Debug,
    P: FnMut(Span<'a>) -> IRes<'a, T>,
{
    move |input| {
        tracing::trace!(target: "parser", "{fun}({input:?})");
        let result = parser(input);
        match &result {
            Ok(_res) => {
                // tracing::trace!(target: "parser", "{}", format!("{fun}({input:?}) -> {res:?}").green());
            },
            Err(_e) => {
                // tracing::trace!(target: "parser", "{}", format!("{fun}({input:?}) -> {e:?}").red());
            },
        }
        result
    }
}

/// A combinator that recognises a comment, starting at a `#`
/// character and ending at the end of the line.
fn comment(input: Span) -> IRes<()> {
    alt((
        value((), pair(tag("#"), is_not("\n\r"))),
        // a comment that immediately precedes the end of the line –
        // this must come after the normal line comment above
        value((), tag("#")),
        value((), multi_comment),
    ))
    .parse(input)
}

fn multi_comment(i: Span) -> IRes<()> {
    // take_until, not is_not: is_not treats "*/" as a character set, so any '*' or '/'
    // inside the comment body (e.g. a /** doc comment */) would end the match early
    value((), delimited(tag("/*"), take_until("*/"), tag("*/"))).parse(i)
}

/// A combinator that recognises an arbitrary amount of whitespace and
/// comments.
pub(crate) fn tws0(input: Span) -> IRes<()> {
    value((), many0(alt((value((), multispace1), comment)))).parse(input)
}

/*
/// A combinator that recognises any non-empty amount of whitespace
/// and comments.
pub(crate) fn tws1(input: Span) -> IRes<()> {
    value((), many1(alt((value((), multispace1), comment))))(input)
}
*/

/// A combinator that creates a parser for a specific token.
pub(crate) fn token<'a>(token: &'a str) -> impl FnMut(Span<'a>) -> IRes<'a, Span<'a>> {
    map_error(tag(token), || ShExParseError::ExpectedToken(token.to_string()))
}

/// A combinator that creates a parser for a specific token,
/// surrounded by trailing whitespace or comments.
pub(crate) fn token_tws<'a>(token: &'a str) -> impl FnMut(Span<'a>) -> IRes<'a, Span<'a>> {
    map_error(delimited(tws0, tag(token), tws0), || {
        ShExParseError::ExpectedToken(token.to_string())
    })
}

/// A combinator that creates a parser for a specific token,
/// surrounded by trailing whitespace or comments.
pub(crate) fn token_tws_no_case<'a>(token: &'a str) -> impl FnMut(Span<'a>) -> IRes<'a, Span<'a>> {
    map_error(delimited(tws0, tag_no_case(token), tws0), || {
        ShExParseError::ExpectedToken(token.to_string())
    })
}

/// A combinator that creates a parser for a case insensitive tag,
/// surrounded by trailing whitespace or comments.
pub(crate) fn tag_no_case_tws<'a>(token: &'a str) -> impl FnMut(Span<'a>) -> IRes<'a, Span<'a>> {
    map_error(delimited(tws0, tag_no_case(token), tws0), || {
        ShExParseError::ExpectedToken(token.to_string())
    })
}

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

    fn parses_comment(input: &str) -> bool {
        multi_comment(Span::new(input)).is_ok()
    }

    #[test]
    fn multi_comment_accepts_stars_and_slashes() {
        assert!(parses_comment("/* plain */"));
        assert!(parses_comment("/** doc comment */"));
        assert!(parses_comment("/* has * star */"));
        assert!(parses_comment("/* has / slash */"));
        assert!(parses_comment("/**/"));
        assert!(parses_comment("/* multi\nline */"));
        assert!(!parses_comment("/* unterminated"));
    }
}
