/**
	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.path;

import dub.internal.logging;

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;


/// Writes `buffer` to a file
public void writeFile(NativePath path, const void[] buffer)
{
	std.file.write(path.toNativeString(), buffer);
}

/// Returns the content of a file
public ubyte[] readFile(NativePath path)
{
	return cast(ubyte[]) std.file.read(path.toNativeString());
}

/// Returns the content of a file as text
public string readText(NativePath path)
{
    return std.file.readText(path.toNativeString());
}

/**
	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 exception 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(const(wchar)* to, const(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 .. $];
	}
}

private bool isWritable(NativePath name)
{
	version (Windows)
	{
		import core.sys.windows.windows;

		return (name.toNativeString.getAttributes & FILE_ATTRIBUTE_READONLY) == 0;
	}
	else version (Posix)
	{
		import core.sys.posix.sys.stat;

		return (name.toNativeString.getAttributes & S_IWUSR) != 0;
	}
	else
		static assert(false, "Needs implementation.");
}

private void makeWritable(NativePath name)
{
	makeWritable(name.toNativeString);
}

private void makeWritable(string name)
{
	version (Windows)
	{
		import core.sys.windows.windows;

		name.setAttributes(name.getAttributes & ~FILE_ATTRIBUTE_READONLY);
	}
	else version (Posix)
	{
		import core.sys.posix.sys.stat;

		name.setAttributes(name.getAttributes | S_IWUSR);
	}
	else
		static assert(false, "Needs implementation.");
}

/**
	Creates a hardlink if possible, a copy otherwise.

	If `from` is read-only and `overwrite` is true, then a copy is made instead
	and `to` is made writable; so that repeating the command will not fail.
*/
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;
		}
	}
	const writeAccessChangeRequired = overwrite && !isWritable(from);
	if (!writeAccessChangeRequired)
	{
		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);
	if (writeAccessChangeRequired)
		to.makeWritable;
}

/**
	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);
}

/// Checks if a directory exists
bool existsDirectory(NativePath path) {
	if( !existsFile(path) ) return false;
	auto fi = getFileInfo(path);
	return fi.isDirectory;
}

/** 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 ensureDirectory(NativePath path)
{
	mkdirRecurse(path.toNativeString());
}

/**
	Enumerates all files in the specified directory.
*/
int delegate(scope int delegate(ref FileInfo)) iterateDirectory(NativePath path)
{
	int iterator(scope int delegate(ref FileInfo) del){
		foreach (DirEntry ent; dirEntries(path.toNativeString(), SpanMode.shallow)) {
			auto fi = makeFileInfo(ent);
			if (auto res = del(fi))
				return res;
		}
		return 0;
	}
	return &iterator;
}

/**
	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;

	/// 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;
	} catch (Exception e) {
		logDiagnostic("Failed to get extended file information for %s: %s", ret.name, e.msg);
	}
	return ret;
}
