Documentation
use std::io::{Result, Error, ErrorKind};
use std::net::{ToSocketAddrs, SocketAddr, UdpSocket};
use std::time::Duration;
use url::{UrlParser, SchemeType};
use num;
use rand::{thread_rng, random, Rng};
use message::packet::{Packet, CoAPOption};
use message::header::MessageType;
use message::response::CoAPResponse;
use message::request::CoAPRequest;
use message::IsMessage;

const DEFAULT_RECEIVE_TIMEOUT: u64 = 5;  // 5s

pub struct CoAPClient {
    socket: UdpSocket,
    peer_addr: SocketAddr,
}

impl CoAPClient {
    /// Create a CoAP client with the specific source and peer address.
    pub fn new_with_specific_source<A: ToSocketAddrs, B: ToSocketAddrs>(bind_addr: A, peer_addr: B) -> Result<CoAPClient> {
        peer_addr.to_socket_addrs().and_then(|mut iter| {
            match iter.next() {
                Some(paddr) => {
                    UdpSocket::bind(bind_addr).and_then(|s| {
                        s.set_read_timeout(Some(Duration::new(DEFAULT_RECEIVE_TIMEOUT, 0)))
                            .and_then(|_| {
                                Ok(CoAPClient {
                                    socket: s,
                                    peer_addr: paddr,
                                })
                            })
                    })
                }
                None => Err(Error::new(ErrorKind::Other, "no address")),
            }
        })
    }

    /// Create a CoAP client with the peer address.
    pub fn new<A: ToSocketAddrs>(addr: A) -> Result<CoAPClient> {
        addr.to_socket_addrs().and_then(|mut iter| {
            match iter.next() {
                Some(SocketAddr::V4(_)) => {
                    Self::new_with_specific_source("0.0.0.0:0", addr)
                }
                Some(SocketAddr::V6(_)) => {
                    Self::new_with_specific_source(":::0", addr)
                }
                None => Err(Error::new(ErrorKind::Other, "no address")),
            }
        })
    }

    /// Execute a request with the coap url and a specific timeout. Default timeout is 5s.
    pub fn request_with_timeout(url: &str, timeout: Option<Duration>) -> Result<CoAPResponse> {
        let mut url_parser = UrlParser::new();
        url_parser.scheme_type_mapper(Self::coap_scheme_type_mapper);

        match url_parser.parse(url) {
            Ok(url_params) => {
                let mut packet = CoAPRequest::new();
                packet.set_version(1);
                packet.set_type(MessageType::Confirmable);
                packet.set_code("0.01");

                let message_id = thread_rng().gen_range(0, num::pow(2u32, 16)) as u16;
                packet.set_message_id(message_id);

                let mut token: Vec<u8> = vec![1, 1, 1, 1];
                for x in token.iter_mut() {
                    *x = random()
                }
                packet.set_token(token.clone());

                let domain = match url_params.domain() {
                    Some(d) => d,
                    None => return Err(Error::new(ErrorKind::InvalidInput, "domain error")),
                };
                let port = url_params.port_or_default().unwrap();

                if let Some(path) = url_params.path() {
                    for p in path.iter() {
                        packet.add_option(CoAPOption::UriPath, p.clone().into_bytes().to_vec());
                    }
                };

                let client = try!(Self::new((domain, port)));
                try!(client.send(&packet));

                try!(client.set_receive_timeout(timeout));
                match client.receive() {
                    Ok(receive_packet) => {
                        if receive_packet.get_message_id() == message_id &&
                           *receive_packet.get_token() == token {
                            return Ok(receive_packet);
                        } else {
                            return Err(Error::new(ErrorKind::Other, "receive invalid data"));
                        }
                    }
                    Err(e) => Err(e),
                }
            }
            Err(_) => Err(Error::new(ErrorKind::InvalidInput, "url error")),
        }
    }

    /// Execute a request with the coap url.
    pub fn request(url: &str) -> Result<CoAPResponse> {
        Self::request_with_timeout(url, Some(Duration::new(DEFAULT_RECEIVE_TIMEOUT, 0)))
    }

    /// Execute a request.
    pub fn send(&self, request: &CoAPRequest) -> Result<()> {
        match request.message.to_bytes() {
            Ok(bytes) => {
                let size = try!(self.socket.send_to(&bytes[..], self.peer_addr));
                if size == bytes.len() {
                    Ok(())
                } else {
                    Err(Error::new(ErrorKind::Other, "send length error"))
                }
            }
            Err(_) => Err(Error::new(ErrorKind::InvalidInput, "packet error")),
        }
    }

    /// Receive a response.
    pub fn receive(&self) -> Result<CoAPResponse> {
        let mut buf = [0; 1500];

        let (nread, _src) = try!(self.socket.recv_from(&mut buf));
        match Packet::from_bytes(&buf[..nread]) {
            Ok(packet) => Ok(CoAPResponse { message: packet }),
            Err(_) => Err(Error::new(ErrorKind::InvalidInput, "packet error")),
        }
    }

    /// Set the receive timeout.
    pub fn set_receive_timeout(&self, dur: Option<Duration>) -> Result<()> {
        self.socket.set_read_timeout(dur)
    }

    fn coap_scheme_type_mapper(scheme: &str) -> SchemeType {
        match scheme {
            "coap" => SchemeType::Relative(5683),
            _ => SchemeType::NonRelative,
        }
    }
}


#[cfg(test)]
mod test {
    use super::*;
    use std::time::Duration;
    use std::io::ErrorKind;
    use message::request::CoAPRequest;
    use message::response::CoAPResponse;
    use server::CoAPServer;

    #[test]
    fn test_request_error_url() {
        assert!(CoAPClient::request("http://127.0.0.1").is_err());
        assert!(CoAPClient::request("coap://127.0.0.").is_err());
        assert!(CoAPClient::request("127.0.0.1").is_err());
    }

    fn request_handler(_: CoAPRequest) -> Option<CoAPResponse> {
        None
    }

    #[test]
    fn test_request_timeout() {
        let mut server = CoAPServer::new("127.0.0.1:5684").unwrap();
        server.handle(request_handler).unwrap();

        let error = CoAPClient::request_with_timeout("coap://127.0.0.1:5684/Rust",
                                                     Some(Duration::new(1, 0)))
            .unwrap_err();
        if cfg!(windows) {
            assert_eq!(error.kind(), ErrorKind::TimedOut);
        } else {
            assert_eq!(error.kind(), ErrorKind::WouldBlock);
        }
    }
}