use errno::{errno};
use log::{debug, error};
use cpython::{NoArgs, ObjectProtocol, PyClone, PyObject, PyResult, Python, PythonObject, PyTuple, ToPyObject};
use std::cell::{RefCell};
use std::cmp;
use std::io::{Write};
use std::net::{TcpStream};
use std::os::unix::io::{RawFd, AsRawFd};

use crate::pyutils::{close_pyobject, with_released_gil, PyGILState_STATE};


// This is the maximum the Linux kernel will write in a single sendfile call.
const SENDFILE_MAXSIZE: usize = 0x7fff_f000;


pub struct SendFileInfo {
    pub content_length: Option<usize>,
    pub blocksize: i64,
    pub offset: libc::off_t,
    pub fd: RawFd,
    pub done: bool
}


impl SendFileInfo {

    pub fn new(fd: RawFd, blocksize: i64) -> Self{
        Self {
            content_length: None,
            blocksize,
            offset: 0,
            fd,
            done: false
        }
    }

    // true: chunk written completely, false: there's more
    pub fn send_file(&mut self, out: &mut TcpStream) -> bool {
        debug!("Sending file");
        let mut count = if self.blocksize < 0 {
            SENDFILE_MAXSIZE
        } else {
            self.blocksize as usize
        };
        if let Some(cl) = self.content_length {
            count = cmp::min(cl - self.offset as usize, count);
        }
        if count == 0 {
            self.done = true;
        } else {
            let res = unsafe {
                libc::sendfile(
                    out.as_raw_fd(),
                    self.fd,
                    &mut self.offset,
                    count)
            };
            match res {
                -1 => {
                    error!("Could not sendfile(), errno: {}", errno());
                    self.done = true
                },
                num_written if num_written < count as isize => {
                    self.done = true
                },
                _ => {
                    debug!("Sendfile number of bytes written: {}", res);
                }
            }
            if let Err(e) = out.flush() {
                error!("Could not flush: {}", e);
            }
        }
        self.done
    }

    fn update_content_length(&mut self, content_length: usize) {
        self.content_length = Some(content_length);
        if self.blocksize > content_length as i64 {
            self.blocksize = content_length as i64;
        }
    }

}


impl Iterator for SendFileInfo {
    type Item = Vec<u8>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.done { None } else { Some(Vec::new()) }
    }

}


py_class!(pub class FileWrapper |py| {
    data filelike: RefCell<PyObject>;
    data sendfileinfo: RefCell<SendFileInfo>;

    def __new__(_cls, filelike: PyObject, blocksize: Option<i64>=None) -> PyResult<Self> {
        let mut filelike = RefCell::new(filelike);
        let blocksize = match blocksize {
            Some(bs) => bs,
            None => -1
        };
        let mut fd: RawFd = -1;
        if let Ok(fdpyob) = filelike.get_mut().call_method(py, "fileno", NoArgs, None) {
            if let Ok(pyfd) = fdpyob.extract(py) {
                fd = pyfd;
            }
        };
        let sendfileinfo = RefCell::new(SendFileInfo::new(fd, blocksize));
        FileWrapper::create_instance(py, filelike, sendfileinfo)
    }

    def close(&self) -> PyResult<PyObject> {
        match close_pyobject(&mut self.filelike(py).borrow_mut(), py) {
            Ok(_) => Ok(py.None()),
            Err(e) => Err(e)
        }
    }

});


pub trait SendFile {
    // Put this in a trait for more flexibility.
    fn sendfileinfo<'p>(&'p self, py: Python<'p>) -> &'p RefCell<SendFileInfo>;
    fn send_file(&mut self, out: &mut TcpStream, gilstate: PyGILState_STATE, py: Python) -> bool;
    fn update_content_length(&mut self, content_length: usize, py: Python);
    // XXX used only for testing
    fn new(py: Python, fd: RawFd, bs: i64) -> PyResult<FileWrapper>;
}


impl SendFile for FileWrapper {

    // public getter
    fn sendfileinfo<'p>(&'p self, py: Python<'p>) -> &'p RefCell<SendFileInfo> {
        self.sendfileinfo(py)
    }

    fn send_file(&mut self, out: &mut TcpStream, gilstate: PyGILState_STATE, py: Python) -> bool {
        with_released_gil(gilstate, || {
            self.sendfileinfo(py).borrow_mut().send_file(out)
        })
    }

    fn update_content_length(&mut self, content_length: usize, py: Python) {
        self.sendfileinfo(py).borrow_mut().update_content_length(content_length);
    }

    fn new(py: Python, fd: RawFd, bs: i64) -> PyResult<FileWrapper> {
        FileWrapper::create_instance(py, RefCell::new(py.None()), RefCell::new(SendFileInfo::new(fd, bs)))
    }

}


impl Iterator for FileWrapper {
    type Item = Vec<u8>;

    fn next(&mut self) -> Option<Self::Item> {
        let py = unsafe { Python::assume_gil_acquired() };
        if self.sendfileinfo(py).borrow().fd != -1 {
            return self.sendfileinfo(py).borrow_mut().next()
        }
        match self.filelike(py).borrow_mut().call_method(
            py, "read", PyTuple::new(
                py,
                &[self.sendfileinfo(py).borrow().blocksize.to_py_object(py).as_object().clone_ref(py)]), None) {
            Ok(bytes) => {
                if bytes != py.None() {
                    match bytes.extract::<Vec<u8>>(py) {
                        Ok(result) => {
                            if !result.is_empty() {
                                Some(result)
                            } else { None }
                        },
                        Err(e) => {
                            debug!("Could not extract from bytes");
                            e.print_and_set_sys_last_vars(py);
                            None
                        }
                    }
                } else { None }
            },
            Err(e) => {
                debug!("Fileno: {}", self.sendfileinfo(py).borrow().fd);
                e.print_and_set_sys_last_vars(py);
                None
            }
        }
    }

}


#[cfg(test)]
mod tests {

    use env_logger;
    use log::{debug};
    use cpython::exc::TypeError;
    use cpython::{NoArgs, ObjectProtocol, PyClone, PyDict, PyErr, Python, PythonObject, PyTuple, ToPyObject};
    use std::io::{Read, Seek, Write};
    use std::net::{TcpListener, TcpStream, SocketAddr};
    use std::sync::mpsc::channel;
    use std::thread;
    use std::os::unix::io::{AsRawFd, RawFd};
    use tempfile::NamedTempFile;

    use crate::filewrapper::{FileWrapper, SendFile};

    fn init() {
        let _ = env_logger::builder().is_test(true).try_init();
    }

    #[test]
    fn test_no_fileno() {
        let gil = Python::acquire_gil();
        let py = gil.python();
        let locals = PyDict::new(py);
        match py.run(r#"
class FL(object):

    def __init__(self):
        self.offset = 0

    def fileno(self):
        return -1

    def read(self, blocksize):
        result = b'Foo 42'[self.offset:self.offset+blocksize]
        self.offset += blocksize
        return result

f = FL()"#, None, Some(&locals)) {
            Ok(_) => {
                let filelike = locals.get_item(py, "f").expect("Could not get file object").as_object().clone_ref(py);
                let fd: RawFd = filelike.call_method(py, "fileno", NoArgs, None).expect("Could not call fileno method").extract(py).expect("Could not extract RawFd");
                let fwtype = py.get_type::<FileWrapper>();
                let bs :i32 = 2;
                let fwany = fwtype.call(py, PyTuple::new(py, &[filelike, bs.to_py_object(py).as_object().clone_ref(py)]), None).unwrap();
                if let Ok(mut fw) = fwany.extract::<FileWrapper>(py) {
                    assert_eq!(fw.sendfileinfo(py).borrow().fd, fd);
                    for chunk in vec![b"Fo", b"o ", b"42"] {
                        match fw.next() {
                            Some(got) => {
                                debug!("Got: {:?}", got);
                                assert_eq!(chunk, &got[..]);
                            },
                            None => {
                                assert!(false);
                            }
                        }
                    }
                } else { assert!(false); }
            },
            Err(e) => {
                e.print_and_set_sys_last_vars(py);
                assert!(false);
            }
        }
    }

    #[test]
    fn test_no_read_method() {
        let gil = Python::acquire_gil();
        let py = gil.python();
        let locals = PyDict::new(py);
        match py.run(r#"
class FL(object):

    def __init__(self):
        self.offset = 0

    def fileno(self):
        return -1

f = FL()"#, None, Some(&locals)) {
            Ok(_) => {
                let filelike = locals.get_item(py, "f").expect("Could not get file object").as_object().clone_ref(py);
                let fwtype = py.get_type::<FileWrapper>();
                let bs : i32 = 2;
                let fwany = fwtype.call(py, PyTuple::new(py, &[filelike, bs.to_py_object(py).as_object().clone_ref(py)]), None).unwrap();
                if let Ok(mut fw) = fwany.extract::<FileWrapper>(py) {
                    match fw.next() {
                        Some(_) => {
                            assert!(false);
                        },
                        None => {
                            assert!(true);
                        }
                    }
                } else { assert!(false); }
            },
            Err(e) => {
                e.print_and_set_sys_last_vars(py);
                assert!(false);
            }
        }
    }

    #[test]
    fn test_bytes_not_convertible() {
        let gil = Python::acquire_gil();
        let py = gil.python();
        let locals = PyDict::new(py);
        match py.run(r#"
class FL(object):

    def __init__(self):
        self.offset = 0

    def read(self, blocksize):
        result = 'öäü'
        self.offset += blocksize
        return result

    def fileno(self):
        return -1

f = FL()"#, None, Some(&locals)) {
            Ok(_) => {
                let filelike = locals.get_item(py, "f").expect("Could not get file object").as_object().clone_ref(py);
                let fwtype = py.get_type::<FileWrapper>();
                let bs : i32 = 2;
                let fwany = fwtype.call(py, PyTuple::new(py, &[filelike, bs.to_py_object(py).as_object().clone_ref(py)]), None).unwrap();
                if let Ok(mut fw) = fwany.extract::<FileWrapper>(py) {
                    match fw.next() {
                        Some(_) => {
                            assert!(false);
                        },
                        None => {
                            assert!(true);
                        }
                    }
                } else { assert!(false); }
            },
            Err(e) => {
                e.print_and_set_sys_last_vars(py);
                assert!(false);
            }
        }
    }

    #[test]
    fn test_send_file() {
        init();
        let gil = Python::acquire_gil();
        let py = gil.python();
        let addr : SocketAddr = "127.0.0.1:0".parse().expect("Failed to parse address");
        let server = TcpListener::bind(addr).expect("Failed to bind address");
        let addr = server.local_addr().unwrap();
        let mut tmp = NamedTempFile::new().unwrap();
        let mut f = tmp.reopen().unwrap();
        f.seek(std::io::SeekFrom::Start(0)).unwrap();
        let fw = FileWrapper::new(py, f.as_raw_fd(), 4).unwrap();
        tmp.write_all(b"Hello World!\n").unwrap();
        let (tx, rx) = channel();
        let (snd, got) = channel();
        let t = thread::spawn(move || {
            let (mut conn, _addr) = server.accept().unwrap();
            let mut buf = [0; 13];
            let snd = snd.clone();
            conn.read(&mut buf).unwrap();
            snd.send(buf).unwrap();
            buf = [0; 13];
            conn.read(&mut buf).unwrap();
            snd.send(buf).unwrap();
            buf = [0; 13];
            conn.read(&mut buf).unwrap();
            snd.send(buf).unwrap();
            buf = [0; 13];
            conn.read(&mut buf).unwrap();
            snd.send(buf).unwrap();
            rx.recv().unwrap();
        });
        let mut connection = TcpStream::connect(addr).expect("Failed to connect");
        let mut sfi = fw.sendfileinfo(py).borrow_mut();
        sfi.send_file(&mut connection);
        let mut b = got.recv().unwrap();
        debug!("send_file chunk 1: {:?}", b);
        assert_eq!(&b[..], b"Hell\0\0\0\0\0\0\0\0\0");
        assert_eq!(sfi.offset, 4);
        let empty : Option<Vec<u8>> = Some(Vec::new());
        assert_eq!(sfi.next(), empty);
        sfi.send_file(&mut connection);
        b = got.recv().unwrap();
        debug!("send_file chunk 2: {:?}", b);
        assert_eq!(&b[..], b"o Wo\0\0\0\0\0\0\0\0\0");
        assert_eq!(sfi.offset, 8);
        assert_eq!(sfi.next(), empty);
        sfi.send_file(&mut connection);
        b = got.recv().unwrap();
        debug!("send_file chunk 3: {:?}", b);
        assert_eq!(&b[..], b"rld!\0\0\0\0\0\0\0\0\0");
        assert_eq!(sfi.offset, 12);
        assert_eq!(sfi.next(), empty);
        sfi.send_file(&mut connection);
        b = got.recv().unwrap();
        debug!("send_file chunk 4: {:?}", b);
        assert_eq!(&b[..], b"\n\0\0\0\0\0\0\0\0\0\0\0\0");
        assert_eq!(sfi.offset, 13);
        assert_eq!(sfi.next(), None);
        tx.send(()).unwrap();
        t.join().unwrap();
    }

    #[test]
    fn test_send_file_updated_content_length() {
        init();
        let gil = Python::acquire_gil();
        let py = gil.python();
        let addr : SocketAddr = "127.0.0.1:0".parse().expect("Failed to parse address");
        let server = TcpListener::bind(addr).expect("Failed to bind address");
        let addr = server.local_addr().unwrap();
        let mut tmp = NamedTempFile::new().unwrap();
        let mut f = tmp.reopen().unwrap();
        f.seek(std::io::SeekFrom::Start(0)).unwrap();
        let mut fw = FileWrapper::new(py, f.as_raw_fd(), 4).unwrap();
        fw.update_content_length(5, py);
        tmp.write_all(b"Hello World!\n").unwrap();
        let (tx, rx) = channel();
        let (snd, got) = channel();
        let t = thread::spawn(move || {
            let (mut conn, _addr) = server.accept().unwrap();
            let mut buf = [0; 13];
            let snd = snd.clone();
            conn.read(&mut buf).unwrap();
            snd.send(buf).unwrap();
            buf = [0; 13];
            conn.read(&mut buf).unwrap();
            snd.send(buf).unwrap();
            rx.recv().unwrap();
        });
        let mut connection = TcpStream::connect(addr).expect("Failed to connect");
        let mut sfi = fw.sendfileinfo(py).borrow_mut();
        sfi.send_file(&mut connection);
        let mut b = got.recv().unwrap();
        debug!("send_file chunk 1: {:?}", b);
        assert_eq!(&b[..], b"Hell\0\0\0\0\0\0\0\0\0");
        assert_eq!(sfi.offset, 4);
        let empty : Option<Vec<u8>> = Some(Vec::new());
        assert_eq!(sfi.next(), empty);
        sfi.send_file(&mut connection);
        b = got.recv().unwrap();
        debug!("send_file chunk 2: {:?}", b);
        assert_eq!(&b[..], b"o\0\0\0\0\0\0\0\0\0\0\0\0");
        assert_eq!(sfi.offset, 5);
        assert_eq!(sfi.next(), empty);
        sfi.send_file(&mut connection);
        assert_eq!(sfi.next(), None);
        tx.send(()).unwrap();
        t.join().unwrap();
    }

    #[test]
    fn test_send_file_content_length_lt_blocksize() {
        init();
        let gil = Python::acquire_gil();
        let py = gil.python();
        let addr : SocketAddr = "127.0.0.1:0".parse().expect("Failed to parse address");
        let server = TcpListener::bind(addr).expect("Failed to bind address");
        let addr = server.local_addr().unwrap();
        let mut tmp = NamedTempFile::new().unwrap();
        let mut f = tmp.reopen().unwrap();
        f.seek(std::io::SeekFrom::Start(0)).unwrap();
        let mut fw = FileWrapper::new(py, f.as_raw_fd(), 7).unwrap();
        fw.update_content_length(5, py);
        let mut sfi = fw.sendfileinfo(py).borrow_mut();
        assert_eq!(sfi.blocksize, 5);
        tmp.write_all(b"Hello World!\n").unwrap();
        let (tx, rx) = channel();
        let (snd, got) = channel();
        let t = thread::spawn(move || {
            let (mut conn, _addr) = server.accept().unwrap();
            let mut buf = [0; 13];
            let snd = snd.clone();
            conn.read(&mut buf).unwrap();
            snd.send(buf).unwrap();
            rx.recv().unwrap();
        });
        let mut connection = TcpStream::connect(addr).expect("Failed to connect");
        sfi.send_file(&mut connection);
        let b = got.recv().unwrap();
        debug!("send_file chunk 1: {:?}", b);
        assert_eq!(&b[..], b"Hello\0\0\0\0\0\0\0\0");
        assert_eq!(sfi.offset, 5);
        let empty : Option<Vec<u8>> = Some(Vec::new());
        assert_eq!(sfi.next(), empty);
        sfi.send_file(&mut connection);
        assert_eq!(sfi.next(), None);
        tx.send(()).unwrap();
        t.join().unwrap();
    }

    #[test]
    fn test_file_wrapper_new_no_args() {
        init();
        let gil = Python::acquire_gil();
        let py = gil.python();
        let fwtype = py.get_type::<FileWrapper>();
        match fwtype.call(py, PyTuple::new(py, &[]), None) {
            Err(e) => {
                assert!(py.get_type::<TypeError>() == e.get_type(py));
                // clear error from Python
                PyErr::fetch(py);
            },
            Ok(_) => assert!(false)
        }
    }
}
