local type contract = import("~/platforms/filesystem.nlua")
local ffi = require("ffi")
local OSX = ffi.os == "OSX"
local X64 = ffi.arch == "x64"
local fs = {} as contract
ffi.cdef([[
	uint32_t GetLastError();
    uint32_t FormatMessageA(
		uint32_t dwFlags,
		const void* lpSource,
		uint32_t dwMessageId,
		uint32_t dwLanguageId,
		char* lpBuffer,
		uint32_t nSize,
		...
	);
]])

local function last_error(): string
	local error_str = ffi.new("uint8_t[?]", 1024)
	local FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
	local FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200
	local error_flags = bit.bor(FORMAT_MESSAGE_FROM_SYSTEM, FORMAT_MESSAGE_IGNORE_INSERTS)
	local code = ffi.C.GetLastError()
	local numout = ffi.C.FormatMessageA(error_flags, nil, code, 0, error_str, 1023, nil)

	if numout ~= 0 then
		local err = ffi.string(error_str, numout)

		if err:sub(-2) == "\r\n" then return err:sub(0, -3) end
	end

	return "no error"
end

do
	local struct = ffi.typeof([[
        struct {
            unsigned long dwFileAttributes;
            uint64_t ftCreationTime;
            uint64_t ftLastAccessTime;
            uint64_t ftLastWriteTime;
            uint64_t nFileSize;
        }
    ]])
	ffi.cdef(
		[[
        int GetFileAttributesExA(const char *lpFileName, int fInfoLevelId, $ *lpFileInformation);
    ]],
		struct
	)

	local function POSIX_TIME(time: number)
		return tonumber(time / 10000000 - 11644473600)
	end

	local flags = {
		archive = 0x20, -- A file or directory that is an archive file or directory. Applications typically use this attribute to mark files for backup or removal .
		compressed = 0x800, -- A file or directory that is compressed. For a file, all of the data in the file is compressed. For a directory, compression is the default for newly created files and subdirectories.
		device = 0x40, -- This value is reserved for system use.
		directory = 0x10, -- The handle that identifies a directory.
		encrypted = 0x4000, -- A file or directory that is encrypted. For a file, all data streams in the file are encrypted. For a directory, encryption is the default for newly created files and subdirectories.
		hidden = 0x2, -- The file or directory is hidden. It is not included in an ordinary directory listing.
		integrity_stream = 0x8000, -- The directory or user data stream is configured with integrity (only supported on ReFS volumes). It is not included in an ordinary directory listing. The integrity setting persists with the file if it's renamed. If a file is copied the destination file will have integrity set if either the source file or destination directory have integrity set.
		normal = 0x80, -- A file that does not have other attributes set. This attribute is valid only when used alone.
		not_content_indexed = 0x2000, -- The file or directory is not to be indexed by the content indexing service.
		no_scrub_data = 0x20000, -- The user data stream not to be read by the background data integrity scanner (AKA scrubber). When set on a directory it only provides inheritance. This flag is only supported on Storage Spaces and ReFS volumes. It is not included in an ordinary directory listing.
		offline = 0x1000, -- The data of a file is not available immediately. This attribute indicates that the file data is physically moved to offline storage. This attribute is used by Remote Storage, which is the hierarchical storage management software. Applications should not arbitrarily change this attribute.
		readonly = 0x1, -- A file that is read-only. Applications can read the file, but cannot write to it or delete it. This attribute is not honored on directories. For more information, see You cannot view or change the Read-only or the System attributes of folders in Windows Server 2003, in Windows XP, in Windows Vista or in Windows 7.
		reparse_point = 0x400, -- A file or directory that has an associated reparse point, or a file that is a symbolic link.
		sparse_file = 0x200, -- A file that is a sparse file.
		system = 0x4, -- A file or directory that the operating system uses a part of, or uses exclusively.
		temporary = 0x100, -- A file that is being used for temporary storage. File systems avoid writing data back to mass storage if sufficient cache memory is available, because typically, an application deletes a temporary file after the handle is closed. In that scenario, the system can entirely avoid writing the data. Otherwise, the data is written after the handle is closed.
		virtual = 0x10000, -- This value is reserved for system use.
	}

	function fs.get_attributes(path, follow_link)
		local info = ffi.new("$[1]", struct)

		if ffi.C.GetFileAttributesExA(path, 0, info) ~= 0 then
			return {
				creation_time = POSIX_TIME(info[0].ftCreationTime),
				last_accessed = POSIX_TIME(info[0].ftLastAccessTime),
				last_modified = POSIX_TIME(info[0].ftLastWriteTime),
				last_changed = -1, -- last permission changes
				size = tonumber(info[0].nFileSize),
				type = bit.band(info[0].dwFileAttributes, flags.directory) == flags.directory and
					"directory" or
					"file",
			}
		end

		return nil, last_error()
	end
end

do
	local struct = ffi.typeof([[
        struct {
            uint32_t dwFileAttributes;

            uint64_t ftCreationTime;
            uint64_t ftLastAccessTime;
            uint64_t ftLastWriteTime;

            uint64_t nFileSize;
            
            uint64_t dwReserved;
        
            char cFileName[260];
            char cAlternateFileName[14];
        }
    ]])
	ffi.cdef(
		[[
        void *FindFirstFileA(const char *lpFileName, $ *find_data);
        int FindNextFileA(void *handle, $ *find_data);
        int FindClose(void *);
	]],
		struct,
		struct
	)
	local dot = string.byte(".")

	local function is_dots(ptr: {[number] = number})
		if ptr[0] == dot then
			if ptr[1] == dot and ptr[2] == 0 then return true end

			if ptr[1] == 0 then return true end
		end

		return false
	end

	local INVALID_FILE = ffi.cast("void *", 0xFFFFFFFFFFFFFFFFULL)

	function fs.get_files(path)
		if path == "" then path = "." end

		if path:sub(-1) ~= "/" then path = path .. "/" end

		local data = ffi.new("$[1]", struct)
		local handle = ffi.C.FindFirstFileA(path .. "*", data)

		if handle == nil then return nil, last_error() end

		local out = {}

		if handle == INVALID_FILE then return out end

		local i = 1

		repeat
			if not is_dots(data[0].cFileName) then
				out[i] = ffi.string(data[0].cFileName)
				i = i + 1
			end		until ffi.C.FindNextFileA(handle, data) == 0

		if ffi.C.FindClose(handle) == 0 then return nil, last_error() end

		return out
	end
end

do
	ffi.cdef([[
        unsigned long GetCurrentDirectoryA(unsigned long length, char *buffer);
        int SetCurrentDirectoryA(const char *path);
	]])

	function fs.set_current_directory(path)
		if ffi.C.chdir(path) == 0 then return true end

		return nil, last_error()
	end

	function fs.get_current_directory()
		local buffer = ffi.new("char[260]")
		local length = ffi.C.GetCurrentDirectoryA(260, buffer)
		local str = ffi.string(buffer, length)
		return (str:gsub("\\", "/"))
	end
end

return fs
