use std::borrow::Cow;
use std::fmt;
use std::iter::Enumerate;
use std::ops::Deref;
use std::str::Chars;

use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};

pub const ENCODED_IN_QUERY: &AsciiSet = &CONTROLS
    .add(b' ')
    .add(b'"')
    .add(b'#')
    .add(b'$')
    .add(b'%')
    .add(b'&')
    .add(b'+')
    .add(b',')
    .add(b'/')
    .add(b':')
    .add(b';')
    .add(b'<')
    .add(b'=')
    .add(b'>')
    .add(b'?')
    .add(b'@')
    .add(b'[')
    .add(b'\\')
    .add(b']')
    .add(b'^')
    .add(b'`')
    .add(b'{')
    .add(b'|')
    .add(b'}');

#[derive(Clone)]
pub(crate) struct QueryParam<'a> {
    source: Source<'a>,
}

#[derive(Clone)]
enum Source<'a> {
    Borrowed(&'a str),
    Owned(String),
}

pub fn url_enc(i: &str) -> Cow<str> {
    utf8_percent_encode(i, ENCODED_IN_QUERY).into()
}

impl<'a> QueryParam<'a> {
    pub fn new_key_value(param: &str, value: &str) -> QueryParam<'static> {
        let s = format!("{}={}", url_enc(param), url_enc(value));
        QueryParam {
            source: Source::Owned(s),
        }
    }

    fn as_str(&self) -> &str {
        match &self.source {
            Source::Borrowed(v) => v,
            Source::Owned(v) => v.as_str(),
        }
    }
}

pub(crate) fn parse_query_params(query_string: &str) -> impl Iterator<Item = QueryParam<'_>> {
    assert!(query_string.is_ascii());
    QueryParamIterator(query_string, query_string.chars().enumerate())
}

struct QueryParamIterator<'a>(&'a str, Enumerate<Chars<'a>>);

impl<'a> Iterator for QueryParamIterator<'a> {
    type Item = QueryParam<'a>;

    fn next(&mut self) -> Option<Self::Item> {
        let mut first = None;
        let mut value = None;
        let mut separator = None;

        for (n, c) in self.1.by_ref() {
            if first.is_none() {
                first = Some(n);
            }
            if value.is_none() && c == '=' {
                value = Some(n + 1);
            }
            if c == '&' {
                separator = Some(n);
                break;
            }
        }

        if let Some(start) = first {
            let end = separator.unwrap_or(self.0.len());
            let chunk = &self.0[start..end];
            return Some(QueryParam {
                source: Source::Borrowed(chunk),
            });
        }

        None
    }
}

impl<'a> fmt::Debug for QueryParam<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_tuple("QueryParam").field(&self.as_str()).finish()
    }
}

impl<'a> fmt::Display for QueryParam<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.source {
            Source::Borrowed(v) => write!(f, "{}", v),
            Source::Owned(v) => write!(f, "{}", v),
        }
    }
}

impl<'a> Deref for QueryParam<'a> {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        self.as_str()
    }
}

impl<'a> PartialEq for QueryParam<'a> {
    fn eq(&self, other: &Self) -> bool {
        self.as_str() == other.as_str()
    }
}

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

    use crate::http::Uri;

    #[test]
    fn query_string_does_not_start_with_question_mark() {
        let u: Uri = "https://foo.com/qwe?abc=qwe".parse().unwrap();
        assert_eq!(u.query(), Some("abc=qwe"));
    }

    #[test]
    fn percent_encoding_is_not_decoded() {
        let u: Uri = "https://foo.com/qwe?abc=%20123".parse().unwrap();
        assert_eq!(u.query(), Some("abc=%20123"));
    }

    #[test]
    fn fragments_are_not_a_thing() {
        let u: Uri = "https://foo.com/qwe?abc=qwe#yaz".parse().unwrap();
        assert_eq!(u.to_string(), "https://foo.com/qwe?abc=qwe");
    }

    fn p(s: &str) -> Vec<String> {
        parse_query_params(s).map(|q| q.to_string()).collect()
    }

    #[test]
    fn parse_query_string() {
        assert_eq!(parse_query_params("").next(), None);
        assert_eq!(p("&"), vec![""]);
        assert_eq!(p("="), vec!["="]);
        assert_eq!(p("&="), vec!["", "="]);
        assert_eq!(p("foo=bar"), vec!["foo=bar"]);
        assert_eq!(p("foo=bar&"), vec!["foo=bar"]);
        assert_eq!(p("foo=bar&foo2=bar2"), vec!["foo=bar", "foo2=bar2"]);
    }

    #[test]
    fn do_not_url_encode_some_things() {
        const NOT_ENCODE: &str = &"!'()*-._~";
        let q = QueryParam::new_key_value("key", NOT_ENCODE);
        assert_eq!(q.as_str(), format!("key={}", NOT_ENCODE));
    }
}
