// Written in the D programming language.

/**
Utilities for manipulating files and scanning directories.

Authors:

$(WEB digitalmars.com, Walter Bright), $(WEB erdani.org, Andrei
Alexandrescu)

Macros:

WIKI = Phobos/StdFile
*/

/*
 *  Copyright (C) 2001-2004 by Digital Mars, www.digitalmars.com
 * Written by Walter Bright, Christopher E. Miller, Andre Fornacon
 *
 *  This software is provided 'as-is', without any express or implied
 *  warranty. In no event will the authors be held liable for any damages
 *  arising from the use of this software.
 *
 *  Permission is granted to anyone to use this software for any purpose,
 *  including commercial applications, and to alter it and redistribute it
 *  freely, subject to the following restrictions:
 *
 *  o  The origin of this software must not be misrepresented; you must not
 *     claim that you wrote the original software. If you use this software
 *     in a product, an acknowledgment in the product documentation would be
 *     appreciated but is not required.
 *  o  Altered source versions must be plainly marked as such, and must not
 *     be misrepresented as being the original software.
 *  o  This notice may not be removed or altered from any source
 *     distribution.
 */

module std.file;

private import std.c.stdio;
private import std.c.stdlib;
private import std.path;
private import std.string;
private import std.regexp;
private import std.gc;
private import std.c.string;
private import std.traits;
private import std.conv;
private import std.contracts;
private import std.utf;
version (unittest) {
    private import std.stdio; // for testing only
}

/* =========================== Win32 ======================= */

version (Win32)
{

private import std.c.windows.windows;
private import std.utf;
private import std.windows.syserror;
private import std.windows.charset;
private import std.date;

bool useWfuncs = true;

static this()
{
    // Win 95, 98, ME do not implement the W functions
    useWfuncs = (GetVersion() < 0x80000000);
}

/***********************************
 * Exception thrown for file I/O errors.
 */

class FileException : Exception
{
    uint errno;			// operating system error code

    this(string name)
    {
	this(name, "file I/O");
    }

    this(string name, string message)
    {
	super(name ~ ": " ~ message);
    }

    this(string name, uint errno)
    {
	this(name, sysErrorString(errno));
	this.errno = errno;
    }
}

/* **********************************
 * Basic File operations.
 */

/********************************************
 * Read file $(D name), return array of bytes read.
 *
 * Throws: $(D FileException) on error.
 */

void[] read(in string name)
{
    DWORD numread;
    HANDLE h;

    if (useWfuncs)
    {
	const(wchar*) namez = std.utf.toUTF16z(name);
	h = CreateFileW(namez,GENERIC_READ,FILE_SHARE_READ,null,OPEN_EXISTING,
	    FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,cast(HANDLE)null);
    }
    else
    {
	const(char*) namez = toMBSz(name);
	h = CreateFileA(namez,GENERIC_READ,FILE_SHARE_READ,null,OPEN_EXISTING,
	    FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,cast(HANDLE)null);
    }

    if (h == INVALID_HANDLE_VALUE)
	goto err1;

    auto size = GetFileSize(h, null);
    if (size == INVALID_FILE_SIZE)
	goto err2;

    auto buf = std.gc.malloc(size);
    if (buf)
	std.gc.hasNoPointers(buf.ptr);

    if (ReadFile(h,buf.ptr,size,&numread,null) != 1)
	goto err2;

    if (numread != size)
	goto err2;

    if (!CloseHandle(h))
	goto err;

    return buf[0 .. size];

err2:
    CloseHandle(h);
err:
    delete buf;
err1:
    throw new FileException(name, GetLastError());
}

/*********************************************
 * Write buffer[] to file name[].
 * Throws: $(D FileException) on error.
 */

void write(in string name, const void[] buffer)
{
    HANDLE h;
    DWORD numwritten;

    if (useWfuncs)
    {
	const(wchar*) namez = std.utf.toUTF16z(name);
	h = CreateFileW(namez,GENERIC_WRITE,0,null,CREATE_ALWAYS,
	    FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,cast(HANDLE)null);
    }
    else
    {
	const(char*) namez = toMBSz(name);
	h = CreateFileA(namez,GENERIC_WRITE,0,null,CREATE_ALWAYS,
	    FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,cast(HANDLE)null);
    }
    if (h == INVALID_HANDLE_VALUE)
	goto err;

    if (WriteFile(h,buffer.ptr,buffer.length,&numwritten,null) != 1)
	goto err2;

    if (buffer.length != numwritten)
	goto err2;
    
    if (!CloseHandle(h))
	goto err;
    return;

err2:
    CloseHandle(h);
err:
    throw new FileException(name, GetLastError());
}


/*********************************************
 * Append buffer[] to file name[].
 * Throws: $(D FileException) on error.
 */

void append(in string name, in void[] buffer)
{
    HANDLE h;
    DWORD numwritten;

    if (useWfuncs)
    {
	const(wchar*) namez = std.utf.toUTF16z(name);
	h = CreateFileW(namez,GENERIC_WRITE,0,null,OPEN_ALWAYS,
	    FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,cast(HANDLE)null);
    }
    else
    {
	const(char*) namez = toMBSz(name);
	h = CreateFileA(namez,GENERIC_WRITE,0,null,OPEN_ALWAYS,
	    FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,cast(HANDLE)null);
    }
    if (h == INVALID_HANDLE_VALUE)
	goto err;

    SetFilePointer(h, 0, null, FILE_END);

    if (WriteFile(h,buffer.ptr,buffer.length,&numwritten,null) != 1)
	goto err2;

    if (buffer.length != numwritten)
	goto err2;
    
    if (!CloseHandle(h))
	goto err;
    return;

err2:
    CloseHandle(h);
err:
    throw new FileException(name, GetLastError());
}


/***************************************************
 * Rename file from[] to to[].
 * Throws: $(D FileException) on error.
 */

void rename(in string from, in string to)
{
    BOOL result = void;

    if (useWfuncs)
	result = MoveFileW(std.utf.toUTF16z(from), std.utf.toUTF16z(to));
    else
	result = MoveFileA(toMBSz(from), toMBSz(to));
    if (!result)
	throw new FileException(to, GetLastError());
}


/***************************************************
 * Delete file name[].
 * Throws: $(D FileException) on error.
 */

void remove(in string name)
{
    BOOL result = void;

    if (useWfuncs)
	result = DeleteFileW(std.utf.toUTF16z(name));
    else
	result = DeleteFileA(toMBSz(name));
    if (!result)
	throw new FileException(name, GetLastError());
}


/***************************************************
 * Get size of file name[].
 * Throws: $(D FileException) on error.
 */

ulong getSize(in string name)
{
    HANDLE findhndl;
    uint resulth;
    uint resultl;

    if (useWfuncs)
    {
	WIN32_FIND_DATAW filefindbuf;

	findhndl = FindFirstFileW(std.utf.toUTF16z(name), &filefindbuf);
	resulth = filefindbuf.nFileSizeHigh;
	resultl = filefindbuf.nFileSizeLow;
    }
    else
    {
	WIN32_FIND_DATA filefindbuf;

	findhndl = FindFirstFileA(toMBSz(name), &filefindbuf);
	resulth = filefindbuf.nFileSizeHigh;
	resultl = filefindbuf.nFileSizeLow;
    }

    if (findhndl == cast(HANDLE)-1)
    {
	throw new FileException(name, GetLastError());
    }
    FindClose(findhndl);
    return (cast(ulong)resulth << 32) + resultl;
}

/*************************
 * Get creation/access/modified times of file $(D name).
 * Throws: $(D FileException) on error.
 */

void getTimes(in string name, out d_time ftc, out d_time fta, out d_time ftm)
{
    HANDLE findhndl;

    if (useWfuncs)
    {
	WIN32_FIND_DATAW filefindbuf;

	findhndl = FindFirstFileW(std.utf.toUTF16z(name), &filefindbuf);
	ftc = std.date.FILETIME2d_time(&filefindbuf.ftCreationTime);
	fta = std.date.FILETIME2d_time(&filefindbuf.ftLastAccessTime);
	ftm = std.date.FILETIME2d_time(&filefindbuf.ftLastWriteTime);
    }
    else
    {
	WIN32_FIND_DATA filefindbuf;

	findhndl = FindFirstFileA(toMBSz(name), &filefindbuf);
	ftc = std.date.FILETIME2d_time(&filefindbuf.ftCreationTime);
	fta = std.date.FILETIME2d_time(&filefindbuf.ftLastAccessTime);
	ftm = std.date.FILETIME2d_time(&filefindbuf.ftLastWriteTime);
    }

    if (findhndl == cast(HANDLE)-1)
    {
	throw new FileException(name, GetLastError());
    }
    FindClose(findhndl);
}


/***************************************************
 * Does file name[] (or directory) exist?
 * Return 1 if it does, 0 if not.
 */

bool exists(in string name)
{
    uint result;

    if (useWfuncs)
	// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/fileio/base/getfileattributes.asp
	result = GetFileAttributesW(std.utf.toUTF16z(name));
    else
	result = GetFileAttributesA(toMBSz(name));

    return result != 0xFFFFFFFF;
}

/***************************************************
 * Get file name[] attributes.
 * Throws: $(D FileException) on error.
 */

uint getAttributes(string name)
{
    uint result;

    if (useWfuncs)
	result = GetFileAttributesW(std.utf.toUTF16z(name));
    else
	result = GetFileAttributesA(toMBSz(name));
    if (result == 0xFFFFFFFF)
    {
	throw new FileException(name, GetLastError());
    }
    return result;
}

/****************************************************
 * Is name[] a file?
 * Throws: $(D FileException) if name[] doesn't exist.
 */

bool isfile(in string name)
{
    return (getAttributes(name) & FILE_ATTRIBUTE_DIRECTORY) == 0;
}

/****************************************************
 * Is name[] a directory?
 * Throws: $(D FileException) if name[] doesn't exist.
 */

bool isdir(in string name)
{
    return (getAttributes(name) & FILE_ATTRIBUTE_DIRECTORY) != 0;
}

/****************************************************
 * Change directory to pathname[].
 * Throws: $(D FileException) on error.
 */

void chdir(in string pathname)
{   BOOL result;

    if (useWfuncs)
	result = SetCurrentDirectoryW(std.utf.toUTF16z(pathname));
    else
	result = SetCurrentDirectoryA(toMBSz(pathname));

    if (!result)
    {
	throw new FileException(pathname, GetLastError());
    }
}

/****************************************************
 * Make directory pathname[].
 * Throws: $(D FileException) on error.
 */

void mkdir(in string pathname)
{   BOOL result = void;

    if (useWfuncs)
	result = CreateDirectoryW(std.utf.toUTF16z(pathname), null);
    else
	result = CreateDirectoryA(toMBSz(pathname), null);

    if (!result)
    {
	throw new FileException(pathname, GetLastError());
    }
}

/****************************************************
 * Remove directory pathname[].
 * Throws: $(D FileException) on error.
 */

void rmdir(in string pathname)
{   BOOL result = void;

    if (useWfuncs)
	result = RemoveDirectoryW(std.utf.toUTF16z(pathname));
    else
	result = RemoveDirectoryA(toMBSz(pathname));

    if (!result)
    {
	throw new FileException(pathname, GetLastError());
    }
}

/****************************************************
 * Get current directory.
 * Throws: $(D FileException) on error.
 */

string getcwd()
{
    if (useWfuncs)
    {
	wchar c;

	auto len = GetCurrentDirectoryW(0, &c);
	if (!len)
	    goto Lerr;
	auto dir = new wchar[len];
	len = GetCurrentDirectoryW(len, dir.ptr);
	if (!len)
	    goto Lerr;
	return std.utf.toUTF8(dir[0 .. len]); // leave off terminating 0
    }
    else
    {
	char c;

	auto len = GetCurrentDirectoryA(0, &c);
	if (!len)
	    goto Lerr;
	auto dir = new char[len];
	len = GetCurrentDirectoryA(len, dir.ptr);
	if (!len)
	    goto Lerr;
	return cast(string)dir[0 .. len];	// leave off terminating 0
    }

Lerr:
    throw new FileException("getcwd", GetLastError());
}

/***************************************************
 * Directory Entry
 */

struct DirEntry
{
    string name;			/// file or directory name
    ulong size = ~0UL;			/// size of file in bytes
    d_time creationTime = d_time_nan;	/// time of file creation
    d_time lastAccessTime = d_time_nan;	/// time file was last accessed
    d_time lastWriteTime = d_time_nan;	/// time file was last written to
    uint attributes;		// Windows file attributes OR'd together

    void init(string path, in WIN32_FIND_DATA *fd)
    {
	wchar[] wbuf;
	size_t clength;
	size_t wlength;
	size_t n;

	clength = std.string.strlen(fd.cFileName.ptr);

	// Convert cFileName[] to unicode
	wlength = MultiByteToWideChar(0,0,fd.cFileName.ptr,clength,null,0);
	if (wlength > wbuf.length)
	    wbuf.length = wlength;
	n = MultiByteToWideChar(0,0,fd.cFileName.ptr,clength,cast(wchar*)wbuf,wlength);
	assert(n == wlength);
	// toUTF8() returns a new buffer
	name = std.path.join(path, std.utf.toUTF8(wbuf[0 .. wlength]));

	size = (cast(ulong)fd.nFileSizeHigh << 32) | fd.nFileSizeLow;
	creationTime = std.date.FILETIME2d_time(&fd.ftCreationTime);
	lastAccessTime = std.date.FILETIME2d_time(&fd.ftLastAccessTime);
	lastWriteTime = std.date.FILETIME2d_time(&fd.ftLastWriteTime);
	attributes = fd.dwFileAttributes;
    }

    void init(string path, in WIN32_FIND_DATAW *fd)
    {
	size_t clength = std.string.wcslen(fd.cFileName.ptr);
	name = std.path.join(path, std.utf.toUTF8(fd.cFileName[0 .. clength]));
	size = (cast(ulong)fd.nFileSizeHigh << 32) | fd.nFileSizeLow;
	creationTime = std.date.FILETIME2d_time(&fd.ftCreationTime);
	lastAccessTime = std.date.FILETIME2d_time(&fd.ftLastAccessTime);
	lastWriteTime = std.date.FILETIME2d_time(&fd.ftLastWriteTime);
	attributes = fd.dwFileAttributes;
    }

    /****
     * Return !=0 if DirEntry is a directory.
     */
    uint isdir()
    {
	return attributes & FILE_ATTRIBUTE_DIRECTORY;
    }

    /****
     * Return !=0 if DirEntry is a file.
     */
    uint isfile()
    {
	return !(attributes & FILE_ATTRIBUTE_DIRECTORY);
    }
}


/***************************************************
 * Return contents of directory pathname[].
 * The names in the contents do not include the pathname.
 * Throws: $(D FileException) on error
 * Example:
 *	This program lists all the files and subdirectories in its
 *	path argument.
 * ----
 * import std.stdio;
 * import std.file;
 *
 * void main(string[] args)
 * {
 *    auto dirs = std.file.listdir(args[1]);
 *
 *    foreach (d; dirs)
 *	writefln(d);
 * }
 * ----
 */

string[] listdir(string pathname)
{
    string[] result;
    
    bool listing(string filename)
    {
	result ~= filename;
	return true; // continue
    }
    
    listdir(pathname, &listing);
    return result;
}


/*****************************************************
 * Return all the files in the directory and its subdirectories
 * that match pattern or regular expression r.
 * Params:
 *	pathname = Directory name
 *	pattern = String with wildcards, such as $(RED "*.d"). The supported
 *		wildcard strings are described under fnmatch() in
 *		$(LINK2 std_path.html, std.path).
 *	r = Regular expression, for more powerful _pattern matching.
 * Example:
 *	This program lists all the files with a "d" extension in
 *	the path passed as the first argument.
 * ----
 * import std.stdio;
 * import std.file;
 *
 * void main(string[] args)
 * {
 *    auto d_source_files = std.file.listdir(args[1], "*.d");
 *
 *    foreach (d; d_source_files)
 *	writefln(d);
 * }
 * ----
 * A regular expression version that searches for all files with "d" or
 * "obj" extensions:
 * ----
 * import std.stdio;
 * import std.file;
 * import std.regexp;
 *
 * void main(string[] args)
 * {
 *    auto d_source_files = std.file.listdir(args[1], RegExp(r"\.(d|obj)$"));
 *
 *    foreach (d; d_source_files)
 *	writefln(d);
 * }
 * ----
 */

string[] listdir(string pathname, string pattern)
{   string[] result;
    
    bool callback(DirEntry* de)
    {
	if (de.isdir)
	    listdir(de.name, &callback);
	else
	{   if (std.path.fnmatch(de.name, pattern))
		result ~= de.name;
	}
	return true; // continue
    }
    
    listdir(pathname, &callback);
    return result;
}

/** Ditto */

string[] listdir(string pathname, RegExp r)
{   string[] result;
    
    bool callback(DirEntry* de)
    {
	if (de.isdir)
	    listdir(de.name, &callback);
	else
	{   if (r.test(de.name))
		result ~= de.name;
	}
	return true; // continue
    }
    
    listdir(pathname, &callback);
    return result;
}

/******************************************************
 * For each file and directory name in pathname[],
 * pass it to the callback delegate.
 *
 * Note:
 *
 * This function is being phased off. New code should use $(D_PARAM
 * dirEntries) (see below).
 * 
 * Params:
 *	callback =	Delegate that processes each
 *			filename in turn. Returns true to
 *			continue, false to stop.
 * Example:
 *	This program lists all the files in its
 *	path argument, including the path.
 * ----
 * import std.stdio;
 * import std.path;
 * import std.file;
 *
 * void main(string[] args)
 * {
 *    auto pathname = args[1];
 *    string[] result;
 *
 *    bool listing(string filename)
 *    {
 *      result ~= std.path.join(pathname, filename);
 *      return true; // continue
 *    }
 *
 *    listdir(pathname, &listing);
 *
 *    foreach (name; result)
 *      writefln("%s", name);
 * }
 * ----
 */

void listdir(in string pathname, bool delegate(string filename) callback)
{
    bool listing(DirEntry* de)
    {
	return callback(std.path.getBaseName(de.name));
    }

    listdir(pathname, &listing);
}

/******************************************************
 * For each file and directory DirEntry in pathname[],
 * pass it to the callback delegate.
 *
 * Note:
 *
 * This function is being phased off. New code should use $(D_PARAM
 * dirEntries) (see below).
 * 
 * Params:
 *	callback =	Delegate that processes each
 *			DirEntry in turn. Returns true to
 *			continue, false to stop.
 * Example:
 *	This program lists all the files in its
 *	path argument and all subdirectories thereof.
 * ----
 * import std.stdio;
 * import std.file;
 *
 * void main(string[] args)
 * {
 *    bool callback(DirEntry* de)
 *    {
 *      if (de.isdir)
 *        listdir(de.name, &callback);
 *      else
 *        writefln(de.name);
 *      return true;
 *    }
 *
 *    listdir(args[1], &callback);
 * }
 * ----
 */

void listdir(in string pathname, bool delegate(DirEntry* de) callback)
{
    string c;
    HANDLE h;
    DirEntry de;

    c = std.path.join(pathname, "*.*");
    if (useWfuncs)
    {
	WIN32_FIND_DATAW fileinfo;

	h = FindFirstFileW(std.utf.toUTF16z(c), &fileinfo);
	if (h != INVALID_HANDLE_VALUE)
	{
	    try
	    {
		do
		{
		    // Skip "." and ".."
		    if (std.string.wcscmp(fileinfo.cFileName.ptr, ".") == 0 ||
			std.string.wcscmp(fileinfo.cFileName.ptr, "..") == 0)
			continue;

		    de.init(pathname, &fileinfo);
		    if (!callback(&de))
			break;
		} while (FindNextFileW(h,&fileinfo) != FALSE);
	    }
	    finally
	    {
		FindClose(h);
	    }
	}
    }
    else
    {
	WIN32_FIND_DATA fileinfo;

	h = FindFirstFileA(toMBSz(c), &fileinfo);
	if (h != INVALID_HANDLE_VALUE)	// should we throw exception if invalid?
	{
	    try
	    {
		do
		{
		    // Skip "." and ".."
		    if (std.string.strcmp(fileinfo.cFileName.ptr, ".") == 0 ||
			std.string.strcmp(fileinfo.cFileName.ptr, "..") == 0)
			continue;

		    de.init(pathname, &fileinfo);
		    if (!callback(&de))
			break;
		} while (FindNextFileA(h,&fileinfo) != FALSE);
	    }
	    finally
	    {
		FindClose(h);
	    }
	}
    }
}

/******************************************
 * Since Win 9x does not support the "W" API's, first convert
 * to wchar, then convert to multibyte using the current code
 * page.
 * (Thanks to yaneurao for this)
 * Deprecated: use std.windows.charset.toMBSz instead.
 */

const(char)* toMBSz(in string s)
{
    return std.windows.charset.toMBSz(s);
}


/***************************************************
 * Copy a file from[] to[].
 */

void copy(in string from, in string to)
{
    invariant result = useWfuncs
	? CopyFileW(std.utf.toUTF16z(from), std.utf.toUTF16z(to), false)
        : CopyFileA(toMBSz(from), toMBSz(to), false);
    if (!result)
         throw new FileException(to, GetLastError);
}


}

/* =========================== linux ======================= */

version (linux)
{

private import std.date;
private import std.c.linux.linux;

/***********************************
 */

class FileException : Exception
{
    uint errno;			// operating system error code

    this(string name)
    {
	this(name, "file I/O");
    }

    this(string name, string message)
    {
	super(name ~ ": " ~ message);
    }

    this(string name, uint errno)
    {
        char[1024] buf = void;
	auto s = strerror_r(errno, buf.ptr, buf.length);
	this(name, std.string.toString(s));
	this.errno = errno;
    }
}

private T cenforce(T)(T condition, lazy string name)
{
    if (!condition) throw new FileException(name, getErrno);
    return condition;
}


/********************************************
 * Read a file.
 * Returns:
 *	array of bytes read
 */

void[] read(string name)
{
    invariant fd = std.c.linux.linux.open(toStringz(name), O_RDONLY);
    cenforce(fd != -1, name);
    scope(exit) std.c.linux.linux.close(fd);

    struct_stat statbuf = void;
    cenforce(std.c.linux.linux.fstat(fd, &statbuf) == 0, name);
    invariant size = statbuf.st_size;
    if (!size) return null;
    auto buf = std.gc.malloc(size);
    enforce(buf, "Out of memory");
    scope(failure) delete buf;
    std.gc.hasNoPointers(buf.ptr);

    cenforce(std.c.linux.linux.read(fd, buf.ptr, size) == size, name);

    return buf[0 .. size];
}

/********************************************
 * Read and validates (using $(XREF utf, validate)) a text file. $(D
 * S) can be a type of array of characters of any width and constancy.
 *
 * Returns: array of characters read
 *
 * Throws: $(D FileException) on file error, $(D UtfException) on UTF
 * decoding error.
 *      
 */

S readText(S = string)(in string name)
{
    auto result = cast(S) read(name);
    std.utf.validate(result);
    return result;
}

// Implementation helper for write and append

private void writeImpl(in string name, in void[] buffer, in uint mode)
{
    invariant fd = std.c.linux.linux.open(toStringz(name),
            mode, 0660);
    cenforce(fd != -1, name);
    {
        scope(failure) std.c.linux.linux.close(fd);
        invariant size = buffer.length;
        cenforce(std.c.linux.linux.write(fd, buffer.ptr, size) == size, name);
    }
    cenforce(std.c.linux.linux.close(fd) == 0, name);
}


/*********************************************
 * Write a file.
 */

void write(in string name, in void[] buffer)
{
    return writeImpl(name, buffer, O_CREAT | O_WRONLY | O_TRUNC);
}

/*********************************************
 * Append to a file.
 */

void append(in string name, in void[] buffer)
{
    return writeImpl(name, buffer, O_APPEND | O_WRONLY | O_CREAT);
}

/***************************************************
 * Rename a file.
 */

void rename(in string from, in string to)
{
    cenforce(std.c.stdio.rename(toStringz(from), toStringz(to)) == -1, to);
}

/***************************************************
 * Delete a file.
 */

void remove(in string name)
{
    cenforce(std.c.stdio.remove(toStringz(name)) != -1, name);
}


/***************************************************
 * Get file size.
 */

ulong getSize(in string name)
{
    struct_stat statbuf = void;
    cenforce(std.c.linux.linux.stat(toStringz(name), &statbuf) == 0, name);
    return statbuf.st_size;
}

unittest
{
    scope(exit) system("rm -f /tmp/deleteme");
    // create a file of size 1
    assert(system("echo > /tmp/deleteme") == 0);
    assert(getSize("/tmp/deleteme") == 1);
    // create a file of size 3
    assert(system("echo ab > /tmp/deleteme") == 0);
    assert(getSize("/tmp/deleteme") == 3);
}

/***************************************************
 * Get file attributes.
 */

uint getAttributes(in string name)
{
    struct_stat statbuf = void;
    cenforce(std.c.linux.linux.stat(toStringz(name), &statbuf) == 0, name);
    return statbuf.st_mode;
}

/*************************
 * Get creation/access/modified times of file $(D name).
 * Throws: $(D FileException) on error.
 */

void getTimes(in string name, out d_time ftc, out d_time fta, out d_time ftm)
{
    struct_stat statbuf = void;
    cenforce(std.c.linux.linux.stat(toStringz(name), &statbuf) == 0, name);
    ftc = cast(d_time) statbuf.st_ctime * std.date.TicksPerSecond;
    fta = cast(d_time) statbuf.st_atime * std.date.TicksPerSecond;
    ftm = cast(d_time) statbuf.st_mtime * std.date.TicksPerSecond;
}

/*************************
 * Set access/modified times of file $(D name).
 * Throws: $(D FileException) on error.
 */

void setTimes(in string name, d_time fta, d_time ftm)
{
    version (linux)
    {
        // utimbuf times = {
        //     cast(__time_t) (fta / std.date.TicksPerSecond),
        //     cast(__time_t) (ftm / std.date.TicksPerSecond) };
        // enforce(utime(toStringz(name), &times) == 0);
        timeval[2] t = void;
        t[0].tv_sec = fta / std.date.TicksPerSecond;
        t[0].tv_usec = cast(long) ((cast(double) fta / std.date.TicksPerSecond)
                * 1_000_000) % 1_000_000;
        t[1].tv_sec = ftm / std.date.TicksPerSecond;
        t[1].tv_usec = cast(long) ((cast(double) ftm / std.date.TicksPerSecond)
                * 1_000_000) % 1_000_000;
        enforce(utimes(toStringz(name), t.ptr) == 0);
    }
    else
    {
        if (true) enforce(false, "Not implemented");
    }
}

unittest
{
    system("touch deleteme") == 0 || assert(false);
    scope(exit) remove("deleteme");
    d_time ftc1, fta1, ftm1;
    getTimes("deleteme", ftc1, fta1, ftm1);
    setTimes("deleteme", fta1 + 1000, ftm1 + 1000);
    d_time ftc2, fta2, ftm2;
    getTimes("deleteme", ftc2, fta2, ftm2);
    assert(fta1 + 1000 == fta2);
    assert(ftm1 + 1000 == ftm2);
}

/**
   Returns the time of the last modification of file $(D name). If the
   file does not exist, throws a $(D FileException).
*/

d_time lastModified(in string name)
{
    struct_stat statbuf = void;
    cenforce(std.c.linux.linux.stat(toStringz(name), &statbuf) == 0, name);
    return cast(d_time) statbuf.st_mtime * std.date.TicksPerSecond;
}

/**
Returns the time of the last modification of file $(D name). If the
file does not exist, returns $(D returnIfMissing).

A frequent usage pattern occurs in build automation tools such as
$(WEB www.gnu.org/software/make, make) or $(WEB
en.wikipedia.org/wiki/Apache_Ant, ant). To check whether file $(D
target) must be rebuilt from file $(D source) (i.e., $(D target) is
older than $(D source) or does not exist), use the comparison below.

----------------------------
if (lastModified(source) >= lastModified(target, d_time.min))
{
    ... must (re)build ...
}
else
{
    ... target's up-to-date ...
}
----------------------------

The code above throws a $(D FileException) if $(D source) does not
exist (as it should). On the other hand, the $(D d_time.min) default
makes a non-existing $(D target) seem infinitely old so the test
correctly prompts building it.

*/

d_time lastModified(string name, d_time returnIfMissing)
{
    struct_stat statbuf = void;
    return std.c.linux.linux.stat(toStringz(name), &statbuf) != 0
        ? returnIfMissing
        : cast(d_time) statbuf.st_mtime * std.date.TicksPerSecond;
}

unittest
{
    system("touch deleteme") == 0 || assert(false);
    scope(exit) remove("deleteme");
    assert(lastModified("deleteme") > 
        lastModified("this file does not exist", d_time.min));
    assert(lastModified("deleteme") > lastModified(__FILE__));
}

/****************************************************
 * Does file/directory exist?
 */

bool exists(in string name)
{
    return access(toStringz(name), 0) == 0;
}

unittest
{
    assert(exists("."));
    assert(!exists("this file does not exist"));
    system("touch deleteme") == 0 || assert(false);
    scope(exit) remove("deleteme");
    assert(exists("deleteme"));
}

/****************************************************
 * Is name a file?
 */

bool isfile(in string name)
{
    return (getAttributes(name) & S_IFMT) == S_IFREG;	// regular file
}

/****************************************************
 * Is name a directory?
 */

bool isdir(in string name)
{
    return (getAttributes(name) & S_IFMT) == S_IFDIR;
}

/****************************************************
 * Change directory.
 */

void chdir(string pathname)
{
    cenforce(std.c.linux.linux.chdir(toStringz(pathname)) == 0, pathname);
}

/****************************************************
 * Make directory.
 */

void mkdir(string pathname)
{
    cenforce(std.c.linux.linux.mkdir(toStringz(pathname), 0777) == 0, pathname);
}

/****************************************************
 * Make directory and all parent directories as needed.
 */

void mkdirRecurse(string pathname)
{
    invariant left = dirname(pathname);
    exists(left) || mkdirRecurse(left);
    mkdir(pathname);
}

/****************************************************
 * Remove directory.
 */

void rmdir(string pathname)
{
    cenforce(std.c.linux.linux.rmdir(toStringz(pathname)) == 0, pathname);
}

/****************************************************
Remove directory and all of its content and subdirectories,
recursively.
 */

void rmdirRecurse(string pathname)
{
    // all children, recursively depth-first
    foreach (DirEntry e; dirEntries(pathname, SpanMode.depth))
    {
        e.isdir ? rmdir(e.name) : remove(e.name);
    }
    // the dir itself
    rmdir(pathname);
}

unittest
{
    auto d = "/tmp/deleteme/a/b/c/d/e/f/g";
    enforce(collectException(mkdir(d)));
    mkdirRecurse(d);
    rmdirRecurse("/tmp/deleteme");
    enforce(!exists("/tmp/deleteme"));
}

/****************************************************
 * Get current directory.
 */

string getcwd()
{
    auto p = cenforce(std.c.linux.linux.getcwd(null, 0),
            "cannot get cwd");
    scope(exit) std.c.stdlib.free(p);
    return p[0 .. std.string.strlen(p)].idup;
}

/***************************************************
 * Directory Entry
 */

struct DirEntry
{
    string name;			/// file or directory name
    ulong _size = ~0UL;			// size of file in bytes
    d_time _creationTime = d_time_nan;	// time of file creation
    d_time _lastAccessTime = d_time_nan; // time file was last accessed
    d_time _lastWriteTime = d_time_nan;	// time file was last written to
    ubyte d_type;
    struct_stat statbuf;
    bool didstat;			// done lazy evaluation of stat()

    void init(string path, dirent *fd)
    {
        invariant len = std.string.strlen(fd.d_name.ptr);
	name = std.path.join(path, fd.d_name[0 .. len].idup);
	d_type = fd.d_type;
	didstat = false;
    }

    bool isdir()
    {
	return (d_type & DT_DIR) != 0;
    }

    bool isfile()
    {
	return (d_type & DT_REG) != 0;
    }

    ulong size()
    {
	ensureStatDone;
	return _size;
    }

    d_time creationTime()
    {
	ensureStatDone;
	return _creationTime;
    }

    d_time lastAccessTime()
    {
	ensureStatDone;
	return _lastAccessTime;
    }

    d_time lastWriteTime()
    {
	ensureStatDone;
	return _lastWriteTime;
    }

    /* This is to support lazy evaluation, because doing stat's is
     * expensive and not always needed.
     */

    void ensureStatDone()
    {
        if (didstat) return;
	enforce(std.c.linux.linux.stat(toStringz(name), &statbuf) == 0,
                "Failed to stat file `"~name~"'");
	_size = statbuf.st_size;
	_creationTime = cast(d_time)statbuf.st_ctime * std.date.TicksPerSecond;
	_lastAccessTime = cast(d_time)statbuf.st_atime * std.date.TicksPerSecond;
	_lastWriteTime = cast(d_time)statbuf.st_mtime * std.date.TicksPerSecond;
	didstat = true;
    }
}


/***************************************************
 * Return contents of directory.
 */

string[] listdir(string pathname)
{
    string[] result;
    
    bool listing(string filename)
    {
	result ~= filename;
	return true; // continue
    }
    
    listdir(pathname, &listing);
    return result;
}

string[] listdir(string pathname, string pattern)
{   string[] result;
    
    bool callback(DirEntry* de)
    {
	if (de.isdir)
	    listdir(de.name, &callback);
	else
	{   if (std.path.fnmatch(de.name, pattern))
		result ~= de.name;
	}
	return true; // continue
    }
    
    listdir(pathname, &callback);
    return result;
}

string[] listdir(string pathname, RegExp r)
{   string[] result;
    
    bool callback(DirEntry* de)
    {
	if (de.isdir)
	    listdir(de.name, &callback);
	else
	{   if (r.test(de.name))
		result ~= de.name;
	}
	return true; // continue
    }
    
    listdir(pathname, &callback);
    return result;
}

void listdir(string pathname, bool delegate(string filename) callback)
{
    bool listing(DirEntry* de)
    {
	return callback(std.path.getBaseName(de.name));
    }

    listdir(pathname, &listing);
}

void listdir(string pathname, bool delegate(DirEntry* de) callback)
{
    auto h = cenforce(opendir(toStringz(pathname)), pathname);
    scope(exit) closedir(h);
    DirEntry de;
    for (dirent* fdata; (fdata = readdir(h)) != null; )
    {
        // Skip "." and ".."
        if (!std.string.strcmp(fdata.d_name.ptr, ".") ||
                !std.string.strcmp(fdata.d_name.ptr, ".."))
            continue;
        de.init(pathname, fdata);
        if (!callback(&de))	    
            break;
    }
}

/***************************************************
 * Copy a file. File timestamps are preserved.
 */

void copy(in string from, in string to)
{
    version (all)
    {
        invariant fd = std.c.linux.linux.open(toStringz(from), O_RDONLY);
        cenforce(fd != -1, from);
        scope(exit) std.c.linux.linux.close(fd);

        struct_stat statbuf = void;
        cenforce(std.c.linux.linux.fstat(fd, &statbuf) == 0, from);

        auto toz = toStringz(to);
        invariant fdw = std.c.linux.linux.open(toz,
                O_CREAT | O_WRONLY | O_TRUNC, 0660);
        cenforce(fdw != -1, from);
        scope(failure) std.c.stdio.remove(toz);
        {
            scope(failure) std.c.linux.linux.close(fdw);
            auto BUFSIZ = 4096u * 16;
            auto buf = std.c.stdlib.malloc(BUFSIZ);
            if (!buf)
            {
                BUFSIZ = 4096;
                buf = enforce(std.c.stdlib.malloc(BUFSIZ), "Out of memory");
            }
            scope(exit) std.c.stdlib.free(buf);
            
            for (size_t size = statbuf.st_size; size; )
            {
                invariant toxfer = (size > BUFSIZ) ? BUFSIZ : size;
                cenforce(std.c.linux.linux.read(fd, buf, toxfer) == toxfer
                        && std.c.linux.linux.write(fdw, buf, toxfer) == toxfer,
                        from);
                assert(size >= toxfer);
                size -= toxfer;
            }
        }
        
        cenforce(std.c.linux.linux.close(fdw) != -1, from);
        
        utimbuf utim = void;
        utim.actime = cast(__time_t) statbuf.st_atime;
        utim.modtime = cast(__time_t) statbuf.st_mtime;
        cenforce(utime(toz, &utim) != -1, from);
    }
    else
    {
        void[] buffer;
        
        buffer = read(from);
        write(to, buffer);
        delete buffer;
    }
}



}

unittest
{
    //printf("std.file.unittest\n");
    void[] buf;

    buf = new void[10];
    (cast(byte[])buf)[] = 3;
    write("unittest_write.tmp", buf);
    void buf2[] = read("unittest_write.tmp");
    assert(buf == buf2);

    copy("unittest_write.tmp", "unittest_write2.tmp");
    buf2 = read("unittest_write2.tmp");
    assert(buf == buf2);

    remove("unittest_write.tmp");
    if (exists("unittest_write.tmp"))
	assert(0);
    remove("unittest_write2.tmp");
    if (exists("unittest_write2.tmp"))
	assert(0);
}

unittest
{
    listdir (".", delegate bool (DirEntry * de)
    {
	auto s = std.string.format("%s : c %s, w %s, a %s", de.name,
		toUTCString (de.creationTime),
		toUTCString (de.lastWriteTime),
		toUTCString (de.lastAccessTime));
	return true;
    }
    );
}

/**
 * Dictates directory spanning policy for $(D_PARAM dirEntries) (see below). 
 */

enum SpanMode
{
    /** Only spans one directory. */
    shallow,
    /** Spans the directory depth-first, i.e. the content of any
     subdirectory is spanned before that subdirectory itself. Useful
     e.g. when recursively deleting files.  */
    depth,
    /** Spans the directory breadth-first, i.e. the content of any
     subdirectory is spanned right after that subdirectory itself. */
    breadth,
}

struct DirIterator
{
    string pathname;
    SpanMode mode;

    private int doIt(D)(D dg, DirEntry * de)
    {
        alias ParameterTypeTuple!(D) Parms;
        static if (is(Parms[0] : string))
        {
            return dg(de.name);
        }
        else static if (is(Parms[0] : DirEntry))
        {
            return dg(*de);
        }
        else
        {
            static assert(false, "Dunno how to enumerate directory entries"
                          " against type " ~ Parms[0].stringof);
        }
    }
    
    int opApply(D)(D dg)
    {
        int result = 0;
        string[] worklist = [ pathname ]; // used only in breadth-first traversal

        bool callback(DirEntry* de)
        {
            switch (mode)
            {
            case SpanMode.shallow:
                result = doIt(dg, de);
                break;
            case SpanMode.breadth:
                result = doIt(dg, de);
                if (!result && de.isdir)
                {
                    worklist ~= de.name;
                }
                break;
            default:
                assert(mode == SpanMode.depth);
                if (de.isdir)
                {
                    listdir(de.name, &callback);
                }
                if (!result)
                {
                    result = doIt(dg, de);
                }
                break;
            }
            return result == 0; 
        }
        
        // consume the worklist
        while (worklist.length)
        {
            auto listThis = worklist[$ - 1];
            worklist.length = worklist.length - 1;
            listdir(listThis, &callback);
        }
        return result;
    }
}

/**
 * Iterates a directory using foreach. The iteration variable can be
 * of type $(D_PARAM string) if only the name is needed, or $(D_PARAM
 * DirEntry) if additional details are needed. The span mode dictates
 * the how the directory is traversed. The name of the directory entry
 * includes the $(D_PARAM path) prefix.
 *
 * Example:
 *
 * ----
 // Iterate a directory in depth
 foreach (string name; dirEntries("destroy/me", SpanMode.depth))
 {
     remove(name);
 }
 // Iterate a directory in breadth
 foreach (string name; dirEntries(".", SpanMode.breadth))
 {
     writeln(name);
 }
 // Iterate a directory and get detailed info about it
 foreach (DirEntry e; dirEntries("dmd-testing", SpanMode.breadth))
 {
     writeln(e.name, "\t", e.size);
 }
 * ----
 */

DirIterator dirEntries(string path, SpanMode mode)
{
    DirIterator result;
    result.pathname = path;
    result.mode = mode;
    return result;
}

unittest
{
  version (linux)
  {
    assert(system("mkdir --parents dmd-testing") == 0);
    scope(exit) system("rm -rf dmd-testing");
    assert(system("mkdir --parents dmd-testing/somedir") == 0);
    assert(system("touch dmd-testing/somefile") == 0);
    assert(system("touch dmd-testing/somedir/somedeepfile") == 0);
    foreach (string name; dirEntries("dmd-testing", SpanMode.shallow))
    {
    }
    foreach (string name; dirEntries("dmd-testing", SpanMode.depth))
    {
        //writeln(name);
    }
    foreach (string name; dirEntries("dmd-testing", SpanMode.breadth))
    {
        //writeln(name);
    }
    foreach (DirEntry e; dirEntries("dmd-testing", SpanMode.breadth))
    {
        //writeln(e.name);
    }
  }
}
