use serde_json::{self, Value};
use hyper::http::HeaderValue;
use hyper::Response;
use hyper::Body;

use std::collections::HashMap;

const APP_JSON_HEADER: &'static str = "application/json";
const CONTENT_TYPE: &'static str = "Content-Type";

pub fn set_json_body(response: &mut Response<Body>, values: Value) {
    response.headers_mut().insert(
        CONTENT_TYPE, 
        HeaderValue::from_static(APP_JSON_HEADER));
    
    *response.body_mut() = Body::from(values.to_string());
}

fn percent_decode(url: &str) -> String {
    // completely ripped from: https://docs.rs/urldecode/0.1.1/src/urldecode/lib.rs.html#1-21
    let url = String::from(url);
    let mut decoded = String::from("");
    let mut skip = 0;
    for i in 0..url.len() {
        if skip != 0 {
            skip -= 1;
            continue;
        }
        let c: char = url.chars().nth(i).unwrap();
        if c == '%' {
            let left = url.chars().nth(i + 1).unwrap();
            let right = url.chars().nth(i + 2).unwrap();
            let byte = u8::from_str_radix(&format!("{}{}", left, right), 16).unwrap();
            decoded += &(byte as char).to_string();
            skip = 2;
        } else {
            decoded += &c.to_string();
        }
    }
    decoded
}

pub fn parse_query_string<'qs>(string: &str) 
        -> HashMap<String, String> {
    
    let mut map: HashMap<String, String> = HashMap::new();

    // get pairs of [key1=value1, key2=value2, ...]
    for pair in string.split('&') {

        let kv: Vec<&str> = pair.split('=').collect();

        match (kv.get(0), kv.get(1)) {
            // only if the format is some_key=some_value do we actually care about it
            (Some(key), Some(value)) => {
                map.insert(percent_decode(&key), percent_decode(&value));
            },
            // ignore all non-pairs
            _ => continue
        };
    }
    return map;
}

// Pulls out Option<type> from a HashMap<&str, &str>
// NOTE: If you need &str just use the hashmap directly
#[macro_export]
macro_rules! qs_param {
    ($obj:expr, $id:literal, $type:ty) => {
        match $obj.get($id) {
            Some(val) => {
                if let Ok(pval) = val.to_string().parse::<$type>() {
                    Some(pval)
                } else{
                    None
                }
            },
            None => None
        }
    }
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;

    #[test]
    fn validate_qs_param() {
        let mut map: HashMap<&str, &str> = HashMap::new();
        map.insert("key", "value");
        map.insert("asdf", "123");
        // It should be noted if you want &str values then just
        // use the hashmap directly, since the macro is a bit clumsy with strings
        // Thust the cast to a String not &str is required
        assert_eq!(qs_param!(map, "key", String).is_some(), true);
        assert_eq!(qs_param!(map, "not-there", String).is_some(), false);

        assert_eq!(qs_param!(map, "asdf", u64).is_some(), true);
        assert_eq!(qs_param!(map, "asdf", bool).is_some(), false);
    }
}
