use common::{Date, Nullable};
use cookie;
use rustc_serialize::json::{self, Json, ToJson};
use std::collections::BTreeMap;
use time;

#[derive(Debug)]
pub enum WebDriverResponse {
    CloseWindow(CloseWindowResponse),
    Cookie(CookieResponse),
    Cookies(CookiesResponse),
    DeleteSession,
    ElementRect(ElementRectResponse),
    Generic(ValueResponse),
    NewSession(NewSessionResponse),
    Timeouts(TimeoutsResponse),
    Void,
    WindowRect(WindowRectResponse),
}

impl WebDriverResponse {
    pub fn to_json_string(self) -> String {
        use response::WebDriverResponse::*;

        let obj = match self {
            CloseWindow(ref x) => json::encode(&x.to_json()),
            Cookie(ref x) => json::encode(x),
            Cookies(ref x) => json::encode(x),
            DeleteSession => Ok("{}".to_string()),
            ElementRect(ref x) => json::encode(x),
            Generic(ref x) => json::encode(x),
            NewSession(ref x) => json::encode(x),
            Timeouts(ref x) => json::encode(x),
            Void => Ok("{}".to_string()),
            WindowRect(ref x) => json::encode(&x.to_json()),
        }.unwrap();

        match self {
            Generic(_) | Cookie(_) | Cookies(_) => obj,
            _ => {
                let mut data = String::with_capacity(11 + obj.len());
                data.push_str("{\"value\": ");
                data.push_str(&*obj);
                data.push_str("}");
                data
            }
        }
    }
}

#[derive(Debug, RustcEncodable)]
pub struct CloseWindowResponse {
    pub window_handles: Vec<String>,
}

impl CloseWindowResponse {
    pub fn new(handles: Vec<String>) -> CloseWindowResponse {
        CloseWindowResponse { window_handles: handles }
    }
}

impl ToJson for CloseWindowResponse {
    fn to_json(&self) -> Json {
        Json::Array(self.window_handles
                    .iter()
                    .map(|x| Json::String(x.clone()))
                    .collect::<Vec<Json>>())
    }
}

#[derive(Debug, RustcEncodable)]
pub struct NewSessionResponse {
    pub sessionId: String,
    pub capabilities: json::Json
}

impl NewSessionResponse {
    pub fn new(session_id: String, capabilities: json::Json) -> NewSessionResponse {
        NewSessionResponse {
            capabilities: capabilities,
            sessionId: session_id
        }
    }
}

#[derive(Debug, RustcEncodable)]
pub struct TimeoutsResponse {
    pub script: u64,
    pub pageLoad: u64,
    pub implicit: u64,
}

impl TimeoutsResponse {
    pub fn new(script: u64, page_load: u64, implicit: u64) -> TimeoutsResponse {
        TimeoutsResponse {
            script: script,
            pageLoad: page_load,
            implicit: implicit,
        }
    }
}

#[derive(Debug, RustcEncodable)]
pub struct ValueResponse {
    pub value: json::Json
}

impl ValueResponse {
    pub fn new(value: json::Json) -> ValueResponse {
        ValueResponse {
            value: value
        }
    }
}

#[derive(Debug, RustcEncodable)]
pub struct ElementRectResponse {
    /// X axis position of the top-left corner of the element relative
    // to the current browsing context’s document element in CSS reference
    // pixels.
    pub x: f64,

    /// Y axis position of the top-left corner of the element relative
    // to the current browsing context’s document element in CSS reference
    // pixels.
    pub y: f64,

    /// Height of the element’s [bounding rectangle] in CSS reference
    /// pixels.
    ///
    /// [bounding rectangle]: https://drafts.fxtf.org/geometry/#rectangle
    pub width: f64,

    /// Width of the element’s [bounding rectangle] in CSS reference
    /// pixels.
    ///
    /// [bounding rectangle]: https://drafts.fxtf.org/geometry/#rectangle
    pub height: f64,
}

#[derive(Debug)]
pub struct WindowRectResponse {
    /// `WindowProxy`’s [screenX] attribute.
    ///
    /// [screenX]: https://drafts.csswg.org/cssom-view/#dom-window-screenx
    pub x: i32,

    /// `WindowProxy`’s [screenY] attribute.
    ///
    /// [screenY]: https://drafts.csswg.org/cssom-view/#dom-window-screeny
    pub y: i32,

    /// Width of the top-level browsing context’s outer dimensions, including
    /// any browser chrome and externally drawn window decorations in CSS
    /// reference pixels.
    pub width: i32,

    /// Height of the top-level browsing context’s outer dimensions, including
    /// any browser chrome and externally drawn window decorations in CSS
    /// reference pixels.
    pub height: i32,
}

impl ToJson for WindowRectResponse {
    fn to_json(&self) -> Json {
        let mut body = BTreeMap::new();
        body.insert("x".to_owned(), self.x.to_json());
        body.insert("y".to_owned(), self.y.to_json());
        body.insert("width".to_owned(), self.width.to_json());
        body.insert("height".to_owned(), self.height.to_json());
        Json::Object(body)
    }
}

#[derive(Clone, Debug, PartialEq, RustcEncodable)]
pub struct Cookie {
    pub name: String,
    pub value: String,
    pub path: Nullable<String>,
    pub domain: Nullable<String>,
    pub expiry: Nullable<Date>,
    pub secure: bool,
    pub httpOnly: bool,
}

impl Into<cookie::Cookie<'static>> for Cookie {
    fn into(self) -> cookie::Cookie<'static> {
        let cookie = cookie::Cookie::build(self.name, self.value)
            .secure(self.secure)
            .http_only(self.httpOnly);
        let cookie = match self.domain {
            Nullable::Value(domain) => cookie.domain(domain),
            Nullable::Null => cookie,
        };
        let cookie = match self.path {
            Nullable::Value(path) => cookie.path(path),
            Nullable::Null => cookie,
        };
        let cookie = match self.expiry {
            Nullable::Value(Date(expiry)) => {
                cookie.expires(time::at(time::Timespec::new(expiry as i64, 0)))
            }
            Nullable::Null => cookie,
        };
        cookie.finish()
    }
}

#[derive(Debug, RustcEncodable)]
pub struct CookieResponse {
    pub value: Cookie,
}

#[derive(Debug, RustcEncodable)]
pub struct CookiesResponse {
    pub value: Vec<Cookie>,
}

#[cfg(test)]
mod tests {
    use super::{CloseWindowResponse, Cookie, CookieResponse, CookiesResponse, ElementRectResponse,
                NewSessionResponse, Nullable, TimeoutsResponse, ValueResponse, WebDriverResponse,
                WindowRectResponse};
    use rustc_serialize::json::Json;
    use std::collections::BTreeMap;

    fn test(resp: WebDriverResponse, expected_str: &str) {
        let data = resp.to_json_string();
        let actual = Json::from_str(&*data).unwrap();
        let expected = Json::from_str(expected_str).unwrap();
        assert_eq!(actual, expected);
    }

    #[test]
    fn test_close_window() {
        let resp = WebDriverResponse::CloseWindow(
            CloseWindowResponse::new(vec!["test".into()]));
        let expected = r#"{"value": ["test"]}"#;
        test(resp, expected);
    }

    #[test]
    fn test_cookie() {
        let cookie = Cookie {
            name: "name".into(),
            value: "value".into(),
            path: Nullable::Value("/".into()),
            domain: Nullable::Null,
            expiry: Nullable::Null,
            secure: true,
            httpOnly: false,
        };
        let resp = WebDriverResponse::Cookie(CookieResponse { value: cookie });
        let expected = r#"{"value": {"name": "name", "expiry": null, "value": "value",
"path": "/", "domain": null, "secure": true, "httpOnly": false}}"#;
        test(resp, expected);
    }

    #[test]
    fn test_cookies() {
        let resp = WebDriverResponse::Cookies(CookiesResponse {
            value: vec![
                Cookie {
                    name: "name".into(),
                    value: "value".into(),
                    path: Nullable::Value("/".into()),
                    domain: Nullable::Null,
                    expiry: Nullable::Null,
                    secure: true,
                    httpOnly: false,
                }
            ]});
        let expected = r#"{"value": [{"name": "name", "value": "value", "path": "/",
"domain": null, "expiry": null, "secure": true, "httpOnly": false}]}"#;
        test(resp, expected);
    }

    #[test]
    fn test_element_rect() {
        let rect = ElementRectResponse {
            x: 0f64,
            y: 1f64,
            width: 2f64,
            height: 3f64,
        };
        let resp = WebDriverResponse::ElementRect(rect);
        let expected = r#"{"value": {"x": 0.0, "y": 1.0, "width": 2.0, "height": 3.0}}"#;
        test(resp, expected);
    }

    #[test]
    fn test_window_rect() {
        let rect = WindowRectResponse {
            x: 0i32,
            y: 1i32,
            width: 2i32,
            height: 3i32,
        };
        let resp = WebDriverResponse::WindowRect(rect);
        let expected = r#"{"value": {"x": 0, "y": 1, "width": 2, "height": 3}}"#;
        test(resp, expected);
    }

    #[test]
    fn test_new_session() {
        let resp = WebDriverResponse::NewSession(
            NewSessionResponse::new("test".into(),
                                    Json::Object(BTreeMap::new())));
        let expected = r#"{"value": {"sessionId": "test", "capabilities": {}}}"#;
        test(resp, expected);
    }

    #[test]
    fn test_timeouts() {
         let resp = WebDriverResponse::Timeouts(TimeoutsResponse::new(
            1, 2, 3));
        let expected = r#"{"value": {"script": 1, "pageLoad": 2, "implicit": 3}}"#;
        test(resp, expected);
    }

    #[test]
    fn test_value() {
        let mut value = BTreeMap::new();
        value.insert("example".into(), Json::Array(vec![Json::String("test".into())]));
        let resp = WebDriverResponse::Generic(ValueResponse::new(
            Json::Object(value)));
        let expected = r#"{"value": {"example": ["test"]}}"#;
        test(resp, expected);
    }
}
