-- This module has not been fully tested against Lua's built-in module.
-- Functionality might be incorrect.

-- TODO: set up __gc in files so they can clean up properly

local io = {}

local file = {}
file.__index = file

local function debugf(...)
    print("DEBUG:", table.unpack({...}))
end

local function not_impl(fnname)
    return function (...)
        error(fnname.." is not implemented")
    end
end

-- some errors should return nil, an error string, then an error number
-- example:
-- > io.open("/nix/abc.txt", "w")
-- nil     /nix/abc.txt: Permission denied 13
-- > io.open("/nix/abc.txt", "r")
-- nil     /nix/abc.txt: No such file or directory 2
-- > a = io.open("abc.txt", "w")
-- > a:read()
-- nil     Bad file descriptor     9
function file.new(filename, mode)
    -- TODO: make this more accurate (disallow opening dirs as files)
    local success, stat, of, allowed
    if mode == nil then
        mode = "r"
    end
    debugf("opening", filename, mode)
    of = setmetatable({_filename=filename, _mode=mode, _seek=0, _open=true, _readable=false, _writable=false, _append_only=false}, file)
    if string.find(mode, "w", 1, true) then
        debugf("opening", filename, "for writing")
        -- preemptively allow writing instead of having that prompt at file:write
        allowed = fs.allow(filename)
        debugf("allowed:", allowed)
        if not allowed then return nil, filename..": Permission denied", 13 end
        -- write mode truncates the file to 0 normally
        success = pcall(fs.truncate, filename, 0)
        if not success then return nil, filename..": Cannot truncate virtual files", 2001 end
        of._stat = {}
        of._size = 0
        of._readable = false
        of._writable = true
    elseif string.find(mode, "r+", 1, true) then
        debugf("opening", filename, "for updating")
        allowed = fs.allow(filename)
        debugf("allowed:", allowed)
        if not allowed then return nil, filename..": Permission denied", 13 end
        success, stat = pcall(fs.stat, filename)
        debugf("stat success:", success)
        if success then
            debugf("type:", stat.type)
            if stat.type == "dir" then return nil end
            of._stat = stat
            of._size = stat.size
        else
            of._stat = {}
            of._size = 0
        end
    elseif string.find(mode, "a", 1, true) then
        debugf("opening", filename, "for appending")
        allowed = fs.allow(filename)
        debugf("allowed:", allowed)
        if not allowed then return nil, filename..": Permission denied", 13 end
        of._append_only = true
        of._writable = true
        success, stat = pcall(fs.stat, filename)
        debugf("stat success:", success)
        if success then
            debugf("type:", stat.type)
            if stat.type == "dir" then return nil end
            of._stat = stat
            of._size = stat.size
        else
            of._stat = {}
            of._size = 0
        end
        if string.find(mode, "+", 1, true) then
            debugf("append update mode")
            of._readable = true
        else
            debugf("append only mode")
            of._readable = false
            of._seek = of._size
        end
    else
        debugf("opening", filename, "for reading")
        -- check if file exists first
        success, stat = pcall(fs.stat, filename)
        debugf("stat success:", success)
        -- lua returns nil if it fails to open for some reason
        if not success then return nil, filename..": No such file or directory", 2 end
        debugf("type:", stat.type)
        if stat.type == "dir" then return nil end
        of._stat = stat
        -- this is so i can adjust the size when data is written
        of._size = stat.size
    end
    debugf("returning of")
    return of
end

function file:_closed_check()
    if not self._open then error("attempt to use a closed file") end
end

function file:_bad_desc()
    debugf("returning bad desc on file", self._filename)
    return nil, "Bad file descriptor", 9
end

function file:close()
    -- yes, this check and error happens even on a closed file
    self:_closed_check()
    self._open = false
    return true
end

function file:flush()
    self:_closed_check()
    -- nothing happens here
end

function file:read(...)
    self:_closed_check()
    if not self._readable then return file:_bad_desc() end
    local to_return = {}
    for i, v in ipairs({...}) do
        if v == "n" or v == "l" or v == "L" then
            error('mode "'..v..'" is not implemented')
        elseif v == "a" then
            local btr = self._size - self._seek
            local data = fs.read_file(self._filename, self._seek, btr)
            self._seek = self._seek + string.len(data)
            table.insert(to_return, data)
        else
            -- assuming this is a number...
            local data = fs.read_file(self._filename, self._seek, v)
            self._seek = self._seek + string.len(data)
            table.insert(to_return, data)
        end
    end
    return table.unpack(to_return)
end

function file:seek(whence, offset)
    if whence == nil then
        whence = "cur"
    end
    if offset == nil then
        offset = 0
    end
    if type(offset) ~= "number" then
        error("bad argument #2 to 'seek' (number expected, got "..type(offset)..")")
    end

    if whence == "set" then
        self._seek = offset
    elseif whence == "cur" then
        self._seek = self._seek + offset
    elseif whence == "end" then
        self._seek = self._size + offset
    else
        error("bad argument #1 to 'seek' (invalid option '"..tostring(whence).."')")
    end

    return self._seek
end

function file:write(...)
    if not self._writable then return file:_bad_desc() end
    local to_write = ''
    for i, v in pairs({...}) do
        to_write = to_write..tostring(v)
    end
    local len = string.len(to_write)
    debugf("attempting to write "..tostring(len).." bytes to "..self._filename)
    if self._append_only then
        self:seek("end")
    end
    local br = fs.write_file(self._filename, self._seek, to_write)
    debugf("wrote "..tostring(br).." bytes to "..self._filename)
    self._seek = self._seek + br
    if self._seek > self._size then
        self._size = self._seek
    end
    return self
end

file.lines = not_impl("file:lines")
file.setvbuf = not_impl("file:setvbuf")

function io.open(filename, mode)
    return file.new(filename, mode)
end

io.close = not_impl("io.close")
io.flush = not_impl("io.flush")
io.lines = not_impl("io.lines")
io.output = not_impl("io.output")
io.popen = not_impl("io.popen")
io.read = not_impl("io.read")
io.tmpfile = not_impl("io.tmpfile")
io.type = not_impl("io.type")
io.write = not_impl("io.write")

return io
