use std::borrow::Cow;

use pear::input::Extent;
use pear::combinators::{prefixed_series, surrounded};
use pear::macros::{parser, switch, parse};
use pear::parsers::*;

use crate::header::{MediaType, Source};
use crate::parse::checkers::{is_whitespace, is_valid_token};

type Input<'a> = pear::input::Pear<pear::input::Cursor<&'a str>>;
type Result<'a, T> = pear::input::Result<T, Input<'a>>;

#[parser]
fn quoted_string<'a>(input: &mut Input<'a>) -> Result<'a, Extent<&'a str>> {
    eat('"')?;

    let mut is_escaped = false;
    let inner = take_while(|&c| {
        if is_escaped { is_escaped = false; return true; }
        if c == '\\' { is_escaped = true; return true; }
        c != '"'
    })?;

    eat('"')?;
    inner
}

#[parser]
fn media_param<'a>(input: &mut Input<'a>) -> Result<'a, (Extent<&'a str>, Extent<&'a str>)> {
    let key = (take_some_while_until(is_valid_token, '=')?, eat('=')?).0;
    let value = switch! {
        peek('"') => quoted_string()?,
        _ => take_some_while_until(is_valid_token, ';')?
    };

    (key, value)
}

#[parser]
pub fn media_type<'a>(input: &mut Input<'a>) -> Result<'a, MediaType> {
    let (top, sub, params) = {
        let top = (take_some_while_until(is_valid_token, '/')?, eat('/')?).0;
        let sub = take_some_while_until(is_valid_token, ';')?;
        let params = prefixed_series(';', |i| {
            let param = surrounded(i, media_param, is_whitespace)?;
            Ok((param.0.into(), param.1.into()))
        }, ';')?;

        (top, sub, params)
    };

    MediaType {
        params,
        source: Source::Custom(Cow::Owned(input.start.to_string())),
        top: top.into(),
        sub: sub.into(),
    }
}

pub fn parse_media_type(input: &str) -> Result<'_, MediaType> {
    parse!(media_type: Input::new(input))
}

#[cfg(test)]
mod test {
    use crate::MediaType;
    use super::parse_media_type;

    macro_rules! assert_no_parse {
        ($string:expr) => ({
            let result: Result<_, _> = parse_media_type($string).into();
            if result.is_ok() {
                panic!("{:?} parsed unexpectedly.", $string)
            }
        });
    }

    macro_rules! assert_parse {
        ($string:expr) => ({
            match parse_media_type($string) {
                Ok(media_type) => media_type,
                Err(e) => panic!("{:?} failed to parse: {}", $string, e)
            }
        });
    }

    macro_rules! assert_parse_eq {
        (@full $string:expr, $result:expr, $(($k:expr, $v:expr)),*) => ({
            let result = assert_parse!($string);
            assert_eq!(result, $result);

            let expected_params: Vec<(&str, &str)> = vec![$(($k, $v)),*];
            if expected_params.len() > 0 {
                assert_eq!(result.params().count(), expected_params.len());
                let all_params = result.params().zip(expected_params.iter());
                for ((key, val), &(ekey, eval)) in all_params {
                    assert_eq!(key, ekey);
                    assert_eq!(val, eval);
                }
            }
        });

        (from: $string:expr, into: $result:expr)
            => (assert_parse_eq!(@full $string, $result, ));
        (from: $string:expr, into: $result:expr, params: $(($key:expr, $val:expr)),*)
            => (assert_parse_eq!(@full $string, $result, $(($key, $val)),*));
    }

    #[test]
    fn check_does_parse() {
        assert_parse!("text/html");
        assert_parse!("a/b");
        assert_parse!("*/*");
    }

    #[test]
    fn check_parse_eq() {
        assert_parse_eq!(from: "text/html", into: MediaType::HTML);
        assert_parse_eq!(from: "text/html; charset=utf-8", into: MediaType::HTML);
        assert_parse_eq!(from: "text/html", into: MediaType::HTML);

        assert_parse_eq!(from: "a/b", into: MediaType::new("a", "b"));
        assert_parse_eq!(from: "*/*", into: MediaType::Any);
        assert_parse_eq!(from: "application/pdf", into: MediaType::PDF);
        assert_parse_eq!(from: "application/json", into: MediaType::JSON);
        assert_parse_eq!(from: "image/svg+xml", into: MediaType::SVG);

        assert_parse_eq!(from: "*/json", into: MediaType::new("*", "json"));
        assert_parse_eq! {
            from: "application/*; param=1",
            into: MediaType::new("application", "*")
        };
    }

    #[test]
    fn check_param_eq() {
        assert_parse_eq! {
            from: "text/html; a=b; b=c; c=d",
            into: MediaType::new("text", "html"),
            params: ("a", "b"), ("b", "c"), ("c", "d")
        };

        assert_parse_eq! {
            from: "text/html;a=b;b=c;     c=d; d=e",
            into: MediaType::new("text", "html"),
            params: ("a", "b"), ("b", "c"), ("c", "d"), ("d", "e")
        };

        assert_parse_eq! {
            from: "text/html; charset=utf-8",
            into: MediaType::new("text", "html"),
            params: ("charset", "utf-8")
        };

        assert_parse_eq! {
            from: "application/*; param=1",
            into: MediaType::new("application", "*"),
            params: ("param", "1")
        };

        assert_parse_eq! {
            from: "*/*;q=0.5;b=c;c=d",
            into: MediaType::Any,
            params: ("q", "0.5"), ("b", "c"), ("c", "d")
        };

        assert_parse_eq! {
            from: "multipart/form-data; boundary=----WebKitFormBoundarypRshfItmvaC3aEuq",
            into: MediaType::FormData,
            params: ("boundary", "----WebKitFormBoundarypRshfItmvaC3aEuq")
        };

        assert_parse_eq! {
            from: r#"*/*; a="hello, world!@#$%^&*();;hi""#,
            into: MediaType::Any,
            params: ("a", "hello, world!@#$%^&*();;hi")
        };

        assert_parse_eq! {
            from: r#"application/json; a=";,;""#,
            into: MediaType::JSON,
            params: ("a", ";,;")
        };

        assert_parse_eq! {
            from: r#"application/json; a=";,;"; b=c"#,
            into: MediaType::JSON,
            params: ("a", ";,;"), ("b", "c")
        };

        assert_parse_eq! {
            from: r#"application/json; b=c; a=";.,.;""#,
            into: MediaType::JSON,
            params: ("b", "c"), ("a", ";.,.;")
        };

        assert_parse_eq! {
            from: r#"*/*; a="a"; b="b"; a=a; b=b; c=c"#,
            into: MediaType::Any,
            params: ("a", "a"), ("b", "b"), ("a", "a"), ("b", "b"), ("c", "c")
        };
    }

    #[test]
    fn check_params_do_parse() {
        assert_parse!("*/*; q=1; q=2");
        assert_parse!("*/*; q=1;q=2;q=3;a=v;c=1;da=1;sdlkldsadasd=uhisdcb89");
        assert_parse!("*/*; q=1; q=2");
        assert_parse!("*/*; q=1; q=2; a=b;c=d;    e=f; a=s;a=e");
        assert_parse!("*/*; q=1; q=2 ; a=b");
        assert_parse!("*/*; q=1; q=2; hello=\"world !\"");
    }

    #[test]
    fn test_bad_parses() {
        assert_no_parse!("*&_/*)()");
        assert_no_parse!("/json");
        assert_no_parse!("text/");
        assert_no_parse!("text//");
        assert_no_parse!("/");
        assert_no_parse!("*/");
        assert_no_parse!("/*");
        assert_no_parse!("///");
        assert_no_parse!("application//json");
        assert_no_parse!("application///json");
        assert_no_parse!("a/b;");
        assert_no_parse!("*/*; a=b;;");
        assert_no_parse!("*/*; a=b;a");
        assert_no_parse!("*/*; a=b; ");
        assert_no_parse!("*/*; a=b;");
        assert_no_parse!("*/*; a = b");
        assert_no_parse!("*/*; a= b");
        assert_no_parse!("*/*; a =b");
        assert_no_parse!(r#"*/*; a="b"#);
        assert_no_parse!(r#"*/*; a="b; c=d"#);
        assert_no_parse!(r#"*/*; a="b; c=d"#);
        assert_no_parse!("*/*;a=@#$%^&*()");
        assert_no_parse!("*/*;;");
        assert_no_parse!("*/*;=;");
        assert_no_parse!("*/*=;");
        assert_no_parse!("*/*=;=");
    }
}
