/**
	File handling.

	Copyright: © 2012 rejectedsoftware e.K.
	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
	Authors: Sönke Ludwig
*/
module dub.internal.vibecompat.core.file;

public import dub.internal.vibecompat.inet.url;

import dub.internal.vibecompat.core.log;

import std.conv;
import core.stdc.stdio;
import std.datetime;
import std.exception;
import std.file;
import std.path;
import std.stdio;
import std.string;
import std.utf;


/* Add output range support to File
*/
struct RangeFile {
@safe:
	std.stdio.File file;

	void put(scope const ubyte[] bytes) @trusted { file.rawWrite(bytes); }
	void put(scope const char[] str) { put(cast(const(ubyte)[])str); }
	void put(char ch) @trusted { put((&ch)[0 .. 1]); }
	void put(dchar ch) { char[4] chars; put(chars[0 .. encode(chars, ch)]); }

	ubyte[] readAll()
	{
		auto sz = this.size;
		enforce(sz <= size_t.max, "File is too big to read to memory.");
		() @trusted { file.seek(0, SEEK_SET); } ();
		auto ret = new ubyte[cast(size_t)sz];
		rawRead(ret);
		return ret;
	}

	void rawRead(ubyte[] dst) @trusted { enforce(file.rawRead(dst).length == dst.length, "Failed to readall bytes from file."); }
	void write(string str) { put(str); }
	void close() @trusted { file.close(); }
	void flush() @trusted { file.flush(); }
	@property ulong size() @trusted { return file.size; }
}


/**
	Opens a file stream with the specified mode.
*/
RangeFile openFile(NativePath path, FileMode mode = FileMode.read)
{
	string fmode;
	final switch(mode){
		case FileMode.read: fmode = "rb"; break;
		case FileMode.readWrite: fmode = "r+b"; break;
		case FileMode.createTrunc: fmode = "wb"; break;
		case FileMode.append: fmode = "ab"; break;
	}
	auto ret = std.stdio.File(path.toNativeString(), fmode);
	assert(ret.isOpen);
	return RangeFile(ret);
}
/// ditto
RangeFile openFile(string path, FileMode mode = FileMode.read)
{
	return openFile(NativePath(path), mode);
}


/**
	Moves or renames a file.
*/
void moveFile(NativePath from, NativePath to)
{
	moveFile(from.toNativeString(), to.toNativeString());
}
/// ditto
void moveFile(string from, string to)
{
	std.file.rename(from, to);
}

/**
	Copies a file.

	Note that attributes and time stamps are currently not retained.

	Params:
		from = NativePath of the source file
		to = NativePath for the destination file
		overwrite = If true, any file existing at the destination path will be
			overwritten. If this is false, an excpetion will be thrown should
			a file already exist at the destination path.

	Throws:
		An Exception if the copy operation fails for some reason.
*/
void copyFile(NativePath from, NativePath to, bool overwrite = false)
{
	enforce(existsFile(from), "Source file does not exist.");

	if (existsFile(to)) {
		enforce(overwrite, "Destination file already exists.");
		// remove file before copy to allow "overwriting" files that are in
		// use on Linux
		removeFile(to);
	}

	static if (is(PreserveAttributes))
	{
		.copy(from.toNativeString(), to.toNativeString(), PreserveAttributes.yes);
	}
	else
	{
		.copy(from.toNativeString(), to.toNativeString());
		// try to preserve ownership/permissions in Posix
		version (Posix) {
			import core.sys.posix.sys.stat;
			import core.sys.posix.unistd;
			import std.utf;
			auto cspath = toUTFz!(const(char)*)(from.toNativeString());
			auto cdpath = toUTFz!(const(char)*)(to.toNativeString());
			stat_t st;
			enforce(stat(cspath, &st) == 0, "Failed to get attributes of source file.");
			if (chown(cdpath, st.st_uid, st.st_gid) != 0)
				st.st_mode &= ~(S_ISUID | S_ISGID);
			chmod(cdpath, st.st_mode);
		}
	}
}
/// ditto
void copyFile(string from, string to)
{
	copyFile(NativePath(from), NativePath(to));
}

version (Windows) extern(Windows) int CreateHardLinkW(in wchar* to, in wchar* from, void* attr=null);

// guess whether 2 files are identical, ignores filename and content
private bool sameFile(NativePath a, NativePath b)
{
	version (Posix) {
		auto st_a = std.file.DirEntry(a.toNativeString).statBuf;
		auto st_b = std.file.DirEntry(b.toNativeString).statBuf;
		return st_a == st_b;
	} else {
		static assert(__traits(allMembers, FileInfo)[0] == "name");
		return getFileInfo(a).tupleof[1 .. $] == getFileInfo(b).tupleof[1 .. $];
	}
}

/**
	Creates a hardlink.
*/
void hardLinkFile(NativePath from, NativePath to, bool overwrite = false)
{
	if (existsFile(to)) {
		enforce(overwrite, "Destination file already exists.");
		if (auto fe = collectException!FileException(removeFile(to))) {
			if (sameFile(from, to)) return;
			throw fe;
		}
	}

	version (Windows)
	{
		alias cstr = toUTFz!(const(wchar)*);
		if (CreateHardLinkW(cstr(to.toNativeString), cstr(from.toNativeString)))
			return;
	}
	else
	{
		import core.sys.posix.unistd : link;
		alias cstr = toUTFz!(const(char)*);
		if (!link(cstr(from.toNativeString), cstr(to.toNativeString)))
			return;
	}
	// fallback to copy
	copyFile(from, to, overwrite);
}

/**
	Removes a file
*/
void removeFile(NativePath path)
{
	removeFile(path.toNativeString());
}
/// ditto
void removeFile(string path) {
	std.file.remove(path);
}

/**
	Checks if a file exists
*/
bool existsFile(NativePath path) {
	return existsFile(path.toNativeString());
}
/// ditto
bool existsFile(string path)
{
	return std.file.exists(path);
}

/** Stores information about the specified file/directory into 'info'

	Returns false if the file does not exist.
*/
FileInfo getFileInfo(NativePath path)
{
	auto ent = std.file.DirEntry(path.toNativeString());
	return makeFileInfo(ent);
}
/// ditto
FileInfo getFileInfo(string path)
{
	return getFileInfo(NativePath(path));
}

/**
	Creates a new directory.
*/
void createDirectory(NativePath path)
{
	mkdir(path.toNativeString());
}
/// ditto
void createDirectory(string path)
{
	createDirectory(NativePath(path));
}

/**
	Enumerates all files in the specified directory.
*/
void listDirectory(NativePath path, scope bool delegate(FileInfo info) del)
{
	foreach( DirEntry ent; dirEntries(path.toNativeString(), SpanMode.shallow) )
		if( !del(makeFileInfo(ent)) )
			break;
}
/// ditto
void listDirectory(string path, scope bool delegate(FileInfo info) del)
{
	listDirectory(NativePath(path), del);
}
/// ditto
int delegate(scope int delegate(ref FileInfo)) iterateDirectory(NativePath path)
{
	int iterator(scope int delegate(ref FileInfo) del){
		int ret = 0;
		listDirectory(path, (fi){
			ret = del(fi);
			return ret == 0;
		});
		return ret;
	}
	return &iterator;
}
/// ditto
int delegate(scope int delegate(ref FileInfo)) iterateDirectory(string path)
{
	return iterateDirectory(NativePath(path));
}


/**
	Returns the current working directory.
*/
NativePath getWorkingDirectory()
{
	return NativePath(std.file.getcwd());
}


/** Contains general information about a file.
*/
struct FileInfo {
	/// Name of the file (not including the path)
	string name;

	/// Size of the file (zero for directories)
	ulong size;

	/// Time of the last modification
	SysTime timeModified;

	/// Time of creation (not available on all operating systems/file systems)
	SysTime timeCreated;

	/// True if this is a symlink to an actual file
	bool isSymlink;

	/// True if this is a directory or a symlink pointing to a directory
	bool isDirectory;
}

/**
	Specifies how a file is manipulated on disk.
*/
enum FileMode {
	/// The file is opened read-only.
	read,
	/// The file is opened for read-write random access.
	readWrite,
	/// The file is truncated if it exists and created otherwise and the opened for read-write access.
	createTrunc,
	/// The file is opened for appending data to it and created if it does not exist.
	append
}

/**
	Accesses the contents of a file as a stream.
*/

private FileInfo makeFileInfo(DirEntry ent)
{
	FileInfo ret;
	ret.name = baseName(ent.name);
	if( ret.name.length == 0 ) ret.name = ent.name;
	assert(ret.name.length > 0);
	ret.isSymlink = ent.isSymlink;
	try {
		ret.isDirectory = ent.isDir;
		ret.size = ent.size;
		ret.timeModified = ent.timeLastModified;
		version(Windows) ret.timeCreated = ent.timeCreated;
		else ret.timeCreated = ent.timeLastModified;
	} catch (Exception e) {
		logDiagnostic("Failed to get extended file information for %s: %s", ret.name, e.msg);
	}
	return ret;
}
