use winnow::combinator::cut_err;
use winnow::combinator::delimited;
use winnow::combinator::opt;
use winnow::combinator::separated;
use winnow::combinator::trace;

use crate::parser::trivia::ws_comment_newline;
use crate::parser::value::value;
use crate::{Array, Item, RawString, Value};

use crate::parser::prelude::*;

// ;; Array

// array = array-open array-values array-close
pub(crate) fn array<'i>(check: RecursionCheck) -> impl Parser<Input<'i>, Array, ContextError> {
    trace("array", move |input: &mut Input<'i>| {
        delimited(
            ARRAY_OPEN,
            cut_err(array_values(check)),
            cut_err(ARRAY_CLOSE)
                .context(StrContext::Label("array"))
                .context(StrContext::Expected(StrContextValue::CharLiteral(']'))),
        )
        .parse_next(input)
    })
}

// note: we're omitting ws and newlines here, because
// they should be part of the formatted values
// array-open  = %x5B ws-newline  ; [
pub(crate) const ARRAY_OPEN: u8 = b'[';
// array-close = ws-newline %x5D  ; ]
const ARRAY_CLOSE: u8 = b']';
// array-sep = ws %x2C ws  ; , Comma
const ARRAY_SEP: u8 = b',';

// note: this rule is modified
// array-values = [ ( array-value array-sep array-values ) /
//                  array-value / ws-comment-newline ]
pub(crate) fn array_values<'i>(
    check: RecursionCheck,
) -> impl Parser<Input<'i>, Array, ContextError> {
    move |input: &mut Input<'i>| {
        let check = check.recursing(input)?;
        (
            opt((
                separated(1.., array_value(check), ARRAY_SEP),
                opt(ARRAY_SEP),
            )
                .map(|(v, trailing): (Vec<Value>, Option<u8>)| {
                    (
                        Array::with_vec(v.into_iter().map(Item::Value).collect()),
                        trailing.is_some(),
                    )
                })),
            ws_comment_newline.span(),
        )
            .try_map::<_, _, std::str::Utf8Error>(|(array, trailing)| {
                let (mut array, comma) = array.unwrap_or_default();
                array.set_trailing_comma(comma);
                array.set_trailing(RawString::with_span(trailing));
                Ok(array)
            })
            .parse_next(input)
    }
}

pub(crate) fn array_value<'i>(
    check: RecursionCheck,
) -> impl Parser<Input<'i>, Value, ContextError> {
    move |input: &mut Input<'i>| {
        (
            ws_comment_newline.span(),
            value(check),
            ws_comment_newline.span(),
        )
            .map(|(ws1, v, ws2)| v.decorated(RawString::with_span(ws1), RawString::with_span(ws2)))
            .parse_next(input)
    }
}

#[cfg(test)]
#[cfg(feature = "parse")]
#[cfg(feature = "display")]
mod test {
    use super::*;

    #[test]
    fn arrays() {
        let inputs = [
            r#"[]"#,
            r#"[   ]"#,
            r#"[
  1, 2, 3
]"#,
            r#"[
  1,
  2, # this is ok
]"#,
            r#"[# comment
# comment2


   ]"#,
            r#"[# comment
# comment2
      1

#sd
,
# comment3

   ]"#,
            r#"[1]"#,
            r#"[1,]"#,
            r#"[ "all", 'strings', """are the same""", '''type''']"#,
            r#"[ 100, -2,]"#,
            r#"[1, 2, 3]"#,
            r#"[1.1, 2.1, 3.1]"#,
            r#"["a", "b", "c"]"#,
            r#"[ [ 1, 2 ], [3, 4, 5] ]"#,
            r#"[ [ 1, 2 ], ["a", "b", "c"] ]"#,
            r#"[ { x = 1, a = "2" }, {a = "a",b = "b",     c =    "c"} ]"#,
        ];
        for input in inputs {
            dbg!(input);
            let mut parsed = array(Default::default()).parse(new_input(input));
            if let Ok(parsed) = &mut parsed {
                parsed.despan(input);
            }
            assert_eq!(parsed.map(|a| a.to_string()), Ok(input.to_owned()));
        }
    }

    #[test]
    fn invalid_arrays() {
        let invalid_inputs = [r#"["#, r#"[,]"#, r#"[,2]"#, r#"[1e165,,]"#];
        for input in invalid_inputs {
            dbg!(input);
            let mut parsed = array(Default::default()).parse(new_input(input));
            if let Ok(parsed) = &mut parsed {
                parsed.despan(input);
            }
            assert!(parsed.is_err());
        }
    }
}
