use std::ffi::CString;
use std::io::{ Read, Write, Seek, SeekFrom, Result };
use std::mem;
use libc::{ c_int, c_char, c_void };
use primitives::*;
use super::{ PhysFSContext, PHYSFS_LOCK };
use super::util::physfs_error_as_io_error;

#[link(name = "physfs")]
extern {
    // valid filehandle on success, NULL on failure
    fn PHYSFS_openAppend(filename: *const c_char) -> *const RawFile;
    fn PHYSFS_openRead(filename: *const c_char) -> *const RawFile;
    fn PHYSFS_openWrite(filename: *const c_char) -> *const RawFile;

    // nonzero on success, 0 on failure (and the handle stays open)
    // The docs make it sound like failure is rare.
    fn PHYSFS_close(file: *const RawFile) -> c_int;

    // Number of bytes read on success, -1 on failure.
    fn PHYSFS_read(file: *const RawFile, buffer: *mut c_void,
                   obj_size: PHYSFS_uint32, obj_count: PHYSFS_uint32) -> PHYSFS_sint64;

    // Number of bytes written on success, -1 on failure.
    fn PHYSFS_write(file: *const RawFile, buffer: *const c_void,
                    obj_size: PHYSFS_uint32, obj_count: PHYSFS_uint32) -> PHYSFS_sint64;

    // Flush buffered file; no-op for unbuffered files.
    fn PHYSFS_flush(file: *const RawFile) -> c_int;

    // Seek to position in file; nonzero on succss, zero on error.
    fn PHYSFS_seek(file: *const RawFile, pos: PHYSFS_uint64) -> c_int;

    // Current position in file, -1 on failure.
    fn PHYSFS_tell(file: *const RawFile) -> PHYSFS_sint64;

    // nonzero if EOF, zero if not.
    fn PHYSFS_eof(file: *const RawFile) -> c_int;

    // Determine file size; returns -1 if impossible
    fn PHYSFS_fileLength(file: *const RawFile) -> PHYSFS_sint64;
}

/// Possible ways to open a file.
#[derive(Copy, Clone)]
pub enum Mode {
    /// Append to the end of the file.
    Append,
    /// Read from the file.
    Read,
    /// Write to the file, overwriting previous data.
    Write,
}

/// A wrapper for the PHYSFS_File type.
#[repr(C)]
struct RawFile {
    opaque: *const c_void,
}

/// A file handle.
#[allow(dead_code)]
pub struct File<'f> {
    raw: *const RawFile,
    mode: Mode,
    context: &'f PhysFSContext,
}

impl<'f> File<'f> {
    /// Opens a file with a specific mode.
    pub fn open<'g>(context: &'g PhysFSContext, filename: String, mode: Mode) -> Result<File<'g>> {
        let _g = PHYSFS_LOCK.lock();
        let c_filename = try!(CString::new(filename));
        let raw = unsafe { match mode {
            Mode::Append => PHYSFS_openAppend(c_filename.as_ptr()),
            Mode::Read => PHYSFS_openRead(c_filename.as_ptr()),
            Mode::Write => PHYSFS_openWrite(c_filename.as_ptr())
        }};

        if raw.is_null() {
            Err(physfs_error_as_io_error())
        }
        else {
            Ok(File{raw: raw, mode: mode, context: context})
        }
    }

    /// Closes a file handle.
    fn close(&self) -> Result<()> {
        let _g = PHYSFS_LOCK.lock();
        match unsafe {
            PHYSFS_close(self.raw)
        } {
            0 => Err(physfs_error_as_io_error()),
            _ => Ok(())
        }
    }

    /// Checks whether eof is reached or not.
    pub fn eof(&self) -> bool {
        let _g = PHYSFS_LOCK.lock();
        let ret = unsafe {
            PHYSFS_eof(self.raw)
        };

        ret != 0
    }

    /// Determine length of file, if possible
    pub fn len(&self) -> Result<u64> {
        let _g = PHYSFS_LOCK.lock();
        let len = unsafe { PHYSFS_fileLength(self.raw) };

        if len >= 0 {
            Ok(len as u64)
        } else {
            Err(physfs_error_as_io_error())
        }
    }

    /// Determines current position within a file
    pub fn tell(&self) -> Result<u64> {
        let _g = PHYSFS_LOCK.lock();
        let ret = unsafe {
            PHYSFS_tell(self.raw)
        };

        match ret {
            -1 => Err(physfs_error_as_io_error()),
            _ => Ok(ret as u64)
        }
    }
}

impl<'f> Read for File<'f> {
    /// Reads from a file
    fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
        let _g = PHYSFS_LOCK.lock();
        let ret = unsafe {
            PHYSFS_read(
                self.raw,
                buf.as_ptr() as *mut c_void,
                mem::size_of::<u8>() as PHYSFS_uint32,
                buf.len() as PHYSFS_uint32
            )
        };

        match ret {
            -1 => Err(physfs_error_as_io_error()),
            _ => Ok(ret as usize)
        }
    }
}

impl<'f> Write for File<'f> {
    /// Writes to a file.
    /// This code performs no safety checks to ensure
    /// that the buffer is the correct length.
    fn write(&mut self, buf: &[u8]) -> Result<usize> {
        let _g = PHYSFS_LOCK.lock();
        let ret = unsafe {
            PHYSFS_write(
                self.raw,
                buf.as_ptr() as *const c_void,
                mem::size_of::<u8>() as PHYSFS_uint32,
                buf.len() as PHYSFS_uint32
            )
        };

        match ret {
            -1 => Err(physfs_error_as_io_error()),
            _ => Ok(ret as usize)
        }
    }

    /// Flushes a file if buffered; no-op if unbuffered.
    fn flush(&mut self) -> Result<()> {
        let _g = PHYSFS_LOCK.lock();
        let ret = unsafe {
            PHYSFS_flush(self.raw)
        };

        match ret {
            0 => Err(physfs_error_as_io_error()),
            _ => Ok(())
        }
    }
}

impl<'f> Seek for File<'f> {
    /// Seek to a new position within a file
    fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
        let seek_pos = match pos {
            SeekFrom::Start(n) => n as i64,
            SeekFrom::End(n) => {
                let len = try!(self.len());
                n + len as i64
            }
            SeekFrom::Current(n) => {
                let curr_pos = try!(self.tell());
                n + curr_pos as i64
            }
        };

        let _g = PHYSFS_LOCK.lock();
        let result = unsafe {
            PHYSFS_seek(
                self.raw,
                seek_pos as PHYSFS_uint64
            )
        };

        if result == -1 {
            return Err(physfs_error_as_io_error());
        }

        self.tell()
    }
}

impl<'f> Drop for File<'f> {
    fn drop(&mut self) {
        let _ = self.close();
    }
}
