// Written in the D programming language.

/**

This module contains cross-platform file access utilities



Synopsis:

----
import exlib.files;
----

Copyright: Vadim Lopatin, 2014
License:   Boost License 1.0
Authors:   Vadim Lopatin, coolreader.org@gmail.com
*/
module dlangui.core.files;

import std.algorithm;

private import dlangui.core.logger;
private import std.process;
private import std.path;
private import std.file;
private import std.utf;

version (Windows) {
    /// path delimiter (\ for windows, / for others)
    immutable char PATH_DELIMITER = '\\';
} else {
    /// path delimiter (\ for windows, / for others)
    immutable char PATH_DELIMITER = '/';
}

/// Filesystem root entry / bookmark types
enum RootEntryType : uint {
    /// filesystem root
    ROOT,
    /// current user home
    HOME,
    /// removable drive
    REMOVABLE,
    /// fixed drive
    FIXED,
    /// network
    NETWORK,
    /// cd rom
    CDROM,
    /// sd card
    SDCARD,
    /// custom bookmark
    BOOKMARK,
}

/// Filesystem root entry item
struct RootEntry {
    private RootEntryType _type;
    private string _path;
    private dstring _display;
    this(RootEntryType type, string path, dstring display = null) {
        _type = type;
        _path = path;
        _display = display;
        if (display is null) {
            _display = toUTF32(baseName(path));
        }
    }
    /// Returns type
    @property RootEntryType type() { return _type; }
    /// Returns path
    @property string path() { return _path; }
    /// Returns display label
    @property dstring label() { return _display; }
    /// Returns icon resource id
    @property string icon() {
        switch (type) {
            case RootEntryType.NETWORK:
                return "folder-network";
            case RootEntryType.BOOKMARK:
                return "folder-bookmark";
            case RootEntryType.CDROM:
                return "drive-optical";
            case RootEntryType.FIXED:
                return "drive-harddisk";
            case RootEntryType.HOME:
                return "user-home";
            case RootEntryType.ROOT:
                return "computer";
            case RootEntryType.SDCARD:
                return "media-flash-sd-mmc";
            case RootEntryType.REMOVABLE:
                return "device-removable-media";
            default:
                return "folder-blue";
        }
    }
}

/// Returns 
@property RootEntry homeEntry() {
    return RootEntry(RootEntryType.HOME, homePath);
}

/// returns array of system root entries
@property RootEntry[] getRootPaths() {
    RootEntry[] res;
    res ~= RootEntry(RootEntryType.HOME, homePath);
    version (posix) {
        res ~= RootEntry(RootEntryType.ROOT, "/", "File System"d);
    }
    version (Windows) {
        import win32.windows;
        uint mask = GetLogicalDrives();
        for (int i = 0; i < 26; i++) {
            if (mask & (1 << i)) {
                char letter = cast(char)('A' + i);
                string path = "" ~ letter ~ ":\\";
                dstring display = ""d ~ letter ~ ":"d;
                // detect drive type
                RootEntryType type;
                uint wtype = GetDriveTypeA(("" ~ path).ptr);
                //Log.d("Drive ", path, " type ", wtype);
                switch (wtype) {
                    case DRIVE_REMOVABLE:
                        type = RootEntryType.REMOVABLE;
                        break;
                    case DRIVE_REMOTE:
                        type = RootEntryType.NETWORK;
                        break;
                    case DRIVE_CDROM:
                        type = RootEntryType.CDROM;
                        break;
                    default:
                        type = RootEntryType.FIXED;
                        break;
                }
                res ~= RootEntry(type, path, display);
            }
        }
    }
    return res;
}

/// returns true if directory is root directory (e.g. / or C:\)
bool isRoot(string path) {
    string root = rootName(path);
    if (path.equal(root))
        return true;
    return false;
}

/// returns parent directory for specified path
string parentDir(string path) {
    return buildNormalizedPath(path, "..");
}

/// check filename with pattern (currently only *.ext pattern is supported)
bool filterFilename(string filename, string pattern) {
	if (pattern.equal("*.*"))
		return true; // matches any
	if (pattern.length < 3)
		return false;
	if (pattern[0] != '*' || pattern[1] != '.')
		return false;
	return filename.endsWith(pattern[1..$]);
}

/// Filters file name by pattern list
bool filterFilename(string filename, string[] filters) {
	if (filters.length == 0)
		return true; // no filters - show all
	foreach(pattern; filters) {
		if (filterFilename(filename, pattern))
			return true;
	}
    return false;
}

/** List directory content 
    
    Optionally filters file names by filter.

    Result will be placed into entries array.

    Returns true if directory exists and listed successfully, false otherwise.
*/
bool listDirectory(string dir, bool includeDirs, bool includeFiles, bool showHiddenFiles, string[] filters, ref DirEntry[] entries) {

    entries.length = 0;

    if (!isDir(dir)) {
        return false;
    }

    if (!isRoot(dir) && includeDirs) {
        entries ~= DirEntry(appendPath(dir, ".."));
    }

    try {
        DirEntry[] dirs;
        DirEntry[] files;
        foreach (DirEntry e; dirEntries(dir, SpanMode.shallow)) {
            string fn = baseName(e.name);
            if (!showHiddenFiles && fn.startsWith("."))
                continue;
            if (e.isDir) {
                dirs ~= e;
            } else if (e.isFile) {
                files ~= e;
            }
        }
        if (includeDirs)
            foreach(DirEntry e; dirs)
                entries ~= e;
        if (includeFiles)
            foreach(DirEntry e; files)
                if (filterFilename(e.name, filters))
                    entries ~= e;
        return true;
    } catch (FileException e) {
        return false;
    }

}

/** Returns true if char ch is / or \ slash */
bool isPathDelimiter(char ch) {
    return ch == '/' || ch == '\\';
}

/// Returns current directory
@property string currentDir() {
    return getcwd();
}

/** Returns current executable path only, including last path delimiter - removes executable name from result of std.file.thisExePath() */
@property string exePath() {
    string path = thisExePath();
    int lastSlash = 0;
    for (int i = 0; i < path.length; i++)
        if (path[i] == PATH_DELIMITER)
            lastSlash = i;
    return path[0 .. lastSlash + 1];
}

/// Returns user's home directory
@property string homePath() {
    string path;
    version (Windows) {
        path = environment.get("USERPROFILE");
        if (path is null)
            path = environment.get("HOME");
    } else {
        path = environment.get("HOME");
    }
    if (path is null)
        path = "."; // fallback to current directory
    return path;
}

/** 
    
    Returns application data directory

    On unix, it will return path to subdirectory in home directory - e.g. /home/user/.subdir if ".subdir" is passed as a paramter.

    On windows, it will return path to subdir in APPDATA directory - e.g. C:\Users\User\AppData\Roaming\.subdir.

 */
string appDataPath(string subdir = null) {
    string path;
    version (Windows) {
        path = environment.get("APPDATA");
    }
    if (path is null)
        path = homePath;
    if (subdir !is null) {
        path ~= PATH_DELIMITER;
        path ~= subdir;
    }
    return path;
}

/// Converts path delimiters to standard for platform inplace in buffer(e.g. / to \ on windows, \ to / on posix), returns buf
char[] convertPathDelimiters(char[] buf) {
    foreach(ref ch; buf) {
        version (Windows) {
            if (ch == '/')
                ch = '\\';
        } else {
            if (ch == '\\')
                ch = '/';
        }
    }
    return buf;
}

/** Converts path delimiters to standard for platform (e.g. / to \ on windows, \ to / on posix) */
string convertPathDelimiters(string src) {
    char[] buf = src.dup;
    return cast(string)convertPathDelimiters(buf);
}

/** Appends file path parts with proper delimiters e.g. appendPath("/home/user", ".myapp", "config") => "/home/user/.myapp/config" */
string appendPath(string[] pathItems ...) {
    char[] buf;
    foreach (s; pathItems) {
        if (buf.length && !isPathDelimiter(buf[$-1]))
            buf ~= PATH_DELIMITER;
        buf ~= s;
    }
    return convertPathDelimiters(buf).dup;
}

/**  Appends file path parts with proper delimiters (as well converts delimiters inside path to system) to buffer e.g. appendPath("/home/user", ".myapp", "config") => "/home/user/.myapp/config" */
char[] appendPath(char[] buf, string[] pathItems ...) {
    foreach (s; pathItems) {
        if (buf.length && !isPathDelimiter(buf[$-1]))
            buf ~= PATH_DELIMITER;
        buf ~= s;
    }
    return convertPathDelimiters(buf);
}

/** Split path into elements, e.g. /home/user/dir1 -> ["home", "user", "dir1"], "c:\dir1\dir2" -> ["c:", "dir1", "dir2"] */
string[] splitPath(string path) {
	string[] res;
	int start = 0;
	for (int i = 0; i <= path.length; i++) {
		char ch = i < path.length ? path[i] : 0;
		if (ch == '\\' || ch == '/' || ch == 0) {
			if (start < i)
				res ~= path[start .. i].dup;
			start = i + 1;
		}
	}
	return res;
}
