use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom};

pub struct FileView {
    file: File,
    start: u64,
    end: u64,
    current: Option<u64>,
}

impl FileView {
    /// Creates a File view between two file offsets. Note that `end` will be
    /// truncated to the actual end of the file.
    pub fn new(mut file: File, start: u64, end: u64) -> io::Result<FileView> {
        let file_end = file.seek(io::SeekFrom::End(0))?;
        file.seek(io::SeekFrom::Start(start))?;
        let end = end.min(file_end);
        Ok(FileView {
            file,
            start,
            end,
            current: Some(start),
        })
    }
}

impl Read for FileView {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        let current = match self.current {
            Some(current) => current,
            None => {
                let current = self.seek(io::SeekFrom::Current(0))? + self.start;
                self.current = Some(current);
                current
            }
        };
        let to_read = buf.len().min((self.end - current) as usize);
        let buf = &mut buf[..to_read];
        match self.file.read(buf) {
            Ok(read) => {
                self.current = Some(current + read as u64);
                Ok(read)
            }
            Err(e) => {
                self.current = None;
                Err(e)
            }
        }
    }
}

impl Seek for FileView {
    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
        match pos {
            SeekFrom::Start(start) => {
                let seek_from = io::SeekFrom::Start(self.end.min(self.start + start));
                match self.file.seek(seek_from) {
                    Ok(new_pos) => {
                        assert!(new_pos >= self.start && new_pos <= self.end);
                        self.current = Some(new_pos);
                        let new_pos = new_pos - self.start;
                        Ok(new_pos)
                    }
                    Err(e) => {
                        self.current = None;
                        Err(e)
                    }
                }
            }
            SeekFrom::End(end) => {
                let end = end.min(0);
                let new_pos = (self.end as i64) + end;
                match self.file.seek(io::SeekFrom::Start(new_pos.max(0) as u64)) {
                    Ok(new_pos) => {
                        assert!(new_pos >= self.start && new_pos <= self.end);
                        self.current = Some(new_pos);
                        let new_pos = new_pos - self.start;
                        Ok(new_pos)
                    }
                    Err(e) => {
                        self.current = None;
                        Err(e)
                    }
                }
            }
            SeekFrom::Current(offset) => {
                let current = match self.current {
                    Some(current) => current,
                    None => {
                        let current = self.seek(io::SeekFrom::Current(0))? + self.start;
                        self.current = Some(current);
                        current
                    }
                };

                let new_pos = (current as i64) + offset;
                let new_pos = new_pos.min(self.end as i64).max(self.start as i64);
                match self.file.seek(io::SeekFrom::Start(new_pos as u64)) {
                    Ok(new_pos) => {
                        assert!(new_pos >= self.start && new_pos <= self.end);
                        self.current = Some(new_pos);
                        let new_pos = new_pos - self.start;
                        Ok(new_pos)
                    }
                    Err(e) => {
                        self.current = None;
                        Err(e)
                    }
                }
            }
        }
    }
}

#[cfg(test)]
mod test {
    use std::io::{BufRead, BufReader};
    use std::path::PathBuf;

    use super::*;

    #[test]
    fn test_file_view_lines() {
        let mut dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        dir.push("resources/test");
        dir.push("small2.bed");

        let f = File::open(dir.clone()).unwrap();
        let file_len = f.metadata().unwrap().len();
        let view = FileView::new(f, 0, file_len).unwrap();
        let mut buf_read = BufReader::new(view);
        let mut str = String::new();

        str.clear();
        let _ = buf_read.read_line(&mut str).unwrap();
        assert_eq!(
            &str,
            "chr5\t65724845\t65725305\t.\t689\t.\t11.08521\t-1.00000\t0.50517\t230\n"
        );

        buf_read.seek(io::SeekFrom::Start(1117)).unwrap();
        str.clear();
        let _ = buf_read.read_line(&mut str).unwrap();
        assert_eq!(
            &str,
            "chr5\t76372720\t76373180\t.\t870\t.\t11.19471\t-1.00000\t0.51655\t230\n"
        );

        buf_read.seek(io::SeekFrom::End(-1366)).unwrap();
        str.clear();
        let _ = buf_read.read_line(&mut str).unwrap();
        assert_eq!(
            &str,
            "chr1\t113141313\t113141773\t.\t562\t.\t11.24802\t-1.00000\t0.52177\t230\n"
        );

        buf_read.seek(io::SeekFrom::Start(1737)).unwrap();
        buf_read.seek(io::SeekFrom::Current(-620)).unwrap();
        str.clear();
        let _ = buf_read.read_line(&mut str).unwrap();
        assert_eq!(
            &str,
            "chr5\t76372720\t76373180\t.\t870\t.\t11.19471\t-1.00000\t0.51655\t230\n"
        );

        buf_read.seek(io::SeekFrom::End(50)).unwrap();
        str.clear();
        let _ = buf_read.read_line(&mut str).unwrap();
        assert_eq!(&str, "");

        let f = File::open(dir.clone()).unwrap();
        let view = FileView::new(f, 0, 1366).unwrap();
        let mut buf_read = BufReader::new(view);
        let mut str = String::new();

        str.clear();
        let _ = buf_read.read_line(&mut str).unwrap();
        assert_eq!(
            &str,
            "chr5\t65724845\t65725305\t.\t689\t.\t11.08521\t-1.00000\t0.50517\t230\n"
        );

        buf_read.seek(io::SeekFrom::Start(1117)).unwrap();
        str.clear();
        let _ = buf_read.read_line(&mut str).unwrap();
        assert_eq!(
            &str,
            "chr5\t76372720\t76373180\t.\t870\t.\t11.19471\t-1.00000\t0.51655\t230\n"
        );

        buf_read.seek(io::SeekFrom::Start(1366)).unwrap();
        buf_read.seek(io::SeekFrom::Current(-249)).unwrap();
        str.clear();
        let _ = buf_read.read_line(&mut str).unwrap();
        assert_eq!(
            &str,
            "chr5\t76372720\t76373180\t.\t870\t.\t11.19471\t-1.00000\t0.51655\t230\n"
        );

        buf_read.seek(io::SeekFrom::End(-1500)).unwrap();
        str.clear();
        let _ = buf_read.read_line(&mut str).unwrap();
        assert_eq!(
            &str,
            "chr5\t65724845\t65725305\t.\t689\t.\t11.08521\t-1.00000\t0.50517\t230\n"
        );

        buf_read.seek(io::SeekFrom::End(50)).unwrap();
        str.clear();
        let _ = buf_read.read_line(&mut str).unwrap();
        assert_eq!(&str, "");

        let f = File::open(dir.clone()).unwrap();
        let view = FileView::new(f, 1117, file_len).unwrap();
        let mut buf_read = BufReader::new(view);
        let mut str = String::new();

        str.clear();
        let _ = buf_read.read_line(&mut str).unwrap();
        assert_eq!(
            &str,
            "chr5\t76372720\t76373180\t.\t870\t.\t11.19471\t-1.00000\t0.51655\t230\n"
        );

        buf_read.seek(io::SeekFrom::Start(620)).unwrap();
        str.clear();
        let _ = buf_read.read_line(&mut str).unwrap();
        assert_eq!(
            &str,
            "chr1\t113141313\t113141773\t.\t562\t.\t11.24802\t-1.00000\t0.52177\t230\n"
        );

        buf_read.seek(io::SeekFrom::End(-1366)).unwrap();
        str.clear();
        let _ = buf_read.read_line(&mut str).unwrap();
        assert_eq!(
            &str,
            "chr1\t113141313\t113141773\t.\t562\t.\t11.24802\t-1.00000\t0.52177\t230\n"
        );

        buf_read.seek(io::SeekFrom::End(50)).unwrap();
        str.clear();
        let _ = buf_read.read_line(&mut str).unwrap();
        assert_eq!(&str, "");
    }

    #[test]
    fn test_file_view_bufs() {
        let mut dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        dir.push("resources/test");
        dir.push("small2.bed");

        let f = File::open(dir.clone()).unwrap();
        let file_len = f.metadata().unwrap().len();

        let mut view = FileView::new(f, 0, file_len).unwrap();

        assert_eq!(
            view.read(&mut vec![0; file_len as usize]).unwrap(),
            file_len as usize
        );

        view.seek(io::SeekFrom::Start(0)).unwrap();
        assert_eq!(
            view.read(&mut vec![0; file_len as usize + 500]).unwrap(),
            file_len as usize
        );

        view.seek(io::SeekFrom::Start(0)).unwrap();
        assert_eq!(view.read(&mut vec![0; 500]).unwrap(), 500);

        let f = File::open(dir.clone()).unwrap();
        let mut view = FileView::new(f, 500, file_len).unwrap();

        assert_eq!(
            view.read(&mut vec![0; file_len as usize]).unwrap(),
            file_len as usize - 500
        );

        view.seek(io::SeekFrom::Start(0)).unwrap();
        assert_eq!(
            view.read(&mut vec![0; file_len as usize + 500]).unwrap(),
            file_len as usize - 500
        );

        view.seek(io::SeekFrom::Start(0)).unwrap();
        assert_eq!(view.read(&mut vec![0; 500]).unwrap(), 500);

        let f = File::open(dir.clone()).unwrap();
        let mut view = FileView::new(f, 500, 1000).unwrap();

        assert_eq!(view.read(&mut vec![0; file_len as usize]).unwrap(), 500);

        view.seek(io::SeekFrom::Start(0)).unwrap();
        assert_eq!(view.read(&mut vec![0; 1000]).unwrap(), 500);

        view.seek(io::SeekFrom::Start(0)).unwrap();
        assert_eq!(view.read(&mut vec![0; 250]).unwrap(), 250);
    }
}
