# This file is a part of Julia. License is MIT: http://julialang.org/license

## UV based file operations ##

module FS

const S_IRUSR = 0o400
const S_IWUSR = 0o200
const S_IXUSR = 0o100
const S_IRWXU = 0o700
const S_IRGRP = 0o040
const S_IWGRP = 0o020
const S_IXGRP = 0o010
const S_IRWXG = 0o070
const S_IROTH = 0o004
const S_IWOTH = 0o002
const S_IXOTH = 0o001
const S_IRWXO = 0o007

export File,
       # open,
       # close,
       write,
       unlink,
       rename,
       sendfile,
       symlink,
       readlink,
       chmod,
       futime,
       JL_O_WRONLY,
       JL_O_RDONLY,
       JL_O_RDWR,
       JL_O_APPEND,
       JL_O_CREAT,
       JL_O_EXCL,
       JL_O_TRUNC,
       JL_O_TEMPORARY,
       JL_O_SHORT_LIVED,
       JL_O_SEQUENTIAL,
       JL_O_RANDOM,
       JL_O_NOCTTY,
       S_IRUSR, S_IWUSR, S_IXUSR, S_IRWXU,
       S_IRGRP, S_IWGRP, S_IXGRP, S_IRWXG,
       S_IROTH, S_IWOTH, S_IXOTH, S_IRWXO

import Base: uvtype, uvhandle, eventloop, fd, position, stat, close, write, read, read!, readbytes, isopen,
            _sizeof_uv_fs, uv_error

include("file_constants.jl")

abstract AbstractFile <: IO

type File <: AbstractFile
    path::AbstractString
    open::Bool
    handle::Int32
    File(path::AbstractString) = new(path,false,-1)
    File(fd::RawFD) = new("",true,fd.fd)
end

type AsyncFile <: AbstractFile
    path::AbstractString
    open::Bool
end

isopen(f::Union{File,AsyncFile}) = f.open

# Not actually a pointer, but that's how we pass it through the C API so it's fine
uvhandle(file::File) = convert(Ptr{Void}, file.handle % UInt)
uvtype(::File) = Base.UV_RAW_FD

_uv_fs_result(req) = ccall(:jl_uv_fs_result,Int32,(Ptr{Void},),req)

function open(f::File,flags::Integer,mode::Integer=0)
    req = Libc.malloc(_sizeof_uv_fs)
    ret = ccall(:uv_fs_open,Int32,(Ptr{Void},Ptr{Void},Cstring,Int32,Int32,Ptr{Void}),
                eventloop(), req, f.path, flags,mode, C_NULL)
    f.handle = _uv_fs_result(req)
    ccall(:uv_fs_req_cleanup,Void,(Ptr{Void},),req)
    Libc.free(req)
    uv_error("open",ret)
    f.open = true
    f
end
open(f::AbstractString,flags,mode) = open(File(f),flags,mode)
open(f::AbstractString,flags) = open(f,flags,0)

function close(f::File)
    if !f.open
        throw(ArgumentError("file \"$(f.path)\" is already closed"))
    end
    err = ccall(:jl_fs_close, Int32, (Int32,), f.handle)
    uv_error("close",err)
    f.handle = -1
    f.open = false
    f
end

function unlink(p::AbstractString)
    err = ccall(:jl_fs_unlink, Int32, (Cstring,), p)
    uv_error("unlink",err)
end
function unlink(f::File)
    if isempty(f.path)
      throw(ArgumentError("no path associated with this file"))
    end
    if f.open
        close(f)
    end
    unlink(f.path)
    f
end

# For move command
function rename(src::AbstractString, dst::AbstractString)
    err = ccall(:jl_fs_rename, Int32, (Cstring, Cstring), src, dst)
    # on error, default to cp && rm
    if err < 0
        # remove_destination: is already done in the mv function
        cp(src, dst; remove_destination=false, follow_symlinks=false)
        rm(src; recursive=true)
    end
end

# For copy command
function sendfile(src::AbstractString, dst::AbstractString)
    src_file = File(src)
    dst_file = File(dst)
    try
        open(src_file, JL_O_RDONLY)
        if !src_file.open
            throw(ArgumentError("source file \"$(src.path)\" is not open"))
        end

        open(dst_file, JL_O_CREAT | JL_O_TRUNC | JL_O_WRONLY,
             S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP| S_IROTH | S_IWOTH)
        if !dst_file.open
            throw(ArgumentError("destination file \"$(dst.path)\" is not open"))
        end

        src_stat = stat(src_file)
        err = ccall(:jl_fs_sendfile, Int32, (Int32, Int32, Int64, Csize_t),
                    fd(src_file), fd(dst_file), 0, src_stat.size)
        uv_error("sendfile", err)
    finally
        if src_file.open
            close(src_file)
        end
        if dst_file.open
            close(dst_file)
        end
    end
end

@windows_only const UV_FS_SYMLINK_JUNCTION = 0x0002
function symlink(p::AbstractString, np::AbstractString)
    @windows_only if Base.windows_version() < Base.WINDOWS_VISTA_VER
        error("Windows XP does not support soft symlinks")
    end
    flags = 0
    @windows_only if isdir(p); flags |= UV_FS_SYMLINK_JUNCTION; p = abspath(p); end
    err = ccall(:jl_fs_symlink, Int32, (Cstring, Cstring, Cint), p, np, flags)
    @windows_only if err < 0 && !isdir(p)
        Base.warn_once("Note: on Windows, creating file symlinks requires Administrator privileges.")
    end
    uv_error("symlink",err)
end

function readlink(path::AbstractString)
    req = Libc.malloc(_sizeof_uv_fs)
    ret = ccall(:uv_fs_readlink, Int32,
        (Ptr{Void}, Ptr{Void}, Cstring, Ptr{Void}),
        eventloop(), req, path, C_NULL)
    uv_error("readlink", ret)
    tgt = bytestring(ccall(:jl_uv_fs_t_ptr, Ptr{Cchar}, (Ptr{Void}, ), req))
    ccall(:uv_fs_req_cleanup, Void, (Ptr{Void}, ), req)
    Libc.free(req)
    tgt
end

function chmod(p::AbstractString, mode::Integer)
    err = ccall(:jl_fs_chmod, Int32, (Cstring, Cint), p, mode)
    uv_error("chmod",err)
end

function write(f::File, buf::Ptr{UInt8}, len::Integer, offset::Integer=-1)
    if !f.open
        throw(ArgumentError("file \"$(f.path)\" is not open"))
    end
    err = ccall(:jl_fs_write, Int32, (Int32, Ptr{UInt8}, Csize_t, Int64),
                f.handle, buf, len, offset)
    uv_error("write",err)
    len
end

write(f::File, c::UInt8) = write(f,[c])

function write{T}(f::File, a::Array{T})
    if isbits(T)
        write(f,pointer(a),length(a)*sizeof(eltype(a)))
    else
        invoke(write, Tuple{IO, Array}, f, a)
    end
end

function truncate(f::File, n::Integer)
    req = Base.Libc.malloc(_sizeof_uv_fs)
    err = ccall(:uv_fs_ftruncate,Int32,(Ptr{Void},Ptr{Void},Int32,Int64,Ptr{Void}),
                eventloop(),req,f.handle,n,C_NULL)
    Libc.free(req)
    uv_error("ftruncate", err)
    f
end

function futime(f::File, atime::Float64, mtime::Float64)
    req = Base.Libc.malloc(_sizeof_uv_fs)
    err = ccall(:uv_fs_futime,Int32,(Ptr{Void},Ptr{Void},Int32,Float64,Float64,Ptr{Void}),
                eventloop(),req,f.handle,atime,mtime,C_NULL)
    Libc.free(req)
    uv_error("futime", err)
    f
end

function read(f::File, ::Type{UInt8})
    if !f.open
        throw(ArgumentError("file \"$(f.path)\" is not open"))
    end
    ret = ccall(:jl_fs_read_byte, Int32, (Int32,), f.handle)
    uv_error("read", ret)
    return ret%UInt8
end

function read!(f::File, a::Vector{UInt8}, nel=length(a))
    if nel < 0 || nel > length(a)
        throw(BoundsError())
    end
    ret = ccall(:jl_fs_read, Int32, (Int32, Ptr{Void}, Csize_t),
                f.handle, a, nel)
    uv_error("read",ret)
    return a
end

nb_available(f::File) = filesize(f) - position(f)

function readbytes!(f::File, b::Array{UInt8}, nb=length(b))
    nr = min(nb, nb_available(f))
    if length(b) < nr
        resize!(b, nr)
    end
    read!(f, b, nr)
    return nr
end
readbytes(io::File) = read!(io, Array(UInt8, nb_available(io)))
readbytes(io::File, nb) = read!(io, Array(UInt8, min(nb, nb_available(io))))

function readbytes(f::File)
    a = Array(UInt8, nb_available(f))
    read!(f,a)
    a
end

const SEEK_SET = Int32(0)
const SEEK_CUR = Int32(1)
const SEEK_END = Int32(2)

function position(f::File)
    ret = ccall(:jl_lseek, Coff_t,(Int32,Coff_t,Int32),f.handle,0,SEEK_CUR)
    systemerror("lseek", ret == -1)
    ret
end

fd(f::File) = RawFD(f.handle)
stat(f::File) = stat(fd(f))

end
