define(function(require, exports, module) {
    "use strict";
    
    main.consumes = ["fs", "Plugin", "c9", "util", "watcher", "error_handler"];
    main.provides = ["fs.cache"];
    return main;

    function main(options, imports, register) {
        var fs = imports.fs;
        var Plugin = imports.Plugin;
        var c9 = imports.c9;
        var util = imports.util;
        var watcher = imports.watcher;
        var reportError = imports.error_handler.reportError;
        var TreeData = require("ace_tree/data_provider");
        var lang = require("ace/lib/lang");
        
        // var basename = require("path").basename;
        var dirname = require("path").dirname;

        /***** Initialization *****/
        
        var plugin = new Plugin("Ajax.org", main.consumes);
        var emit = plugin.getEmitter();
        
        var model = new TreeData();
        var showHidden = false;
        var hiddenFilePattern = "";
        var hiddenFileRe = /^$/;
        
        var orphans = {};
        
        model.loadChildren = function(node, cb) {
            fs.readdir(node.path, function(err, files) {
                cb && cb(err, files);
            });
        };
        model.shouldLoadChildren = function(node, ch) {
            return node.status == "pending"
                || (node.path && node.isFolder && !ch);
        };
        model.getClassName = function(node) {
            var cl = node.className || "";
            if (node.link)
                cl += " symlink";
            if (node.isCut)
                cl += " cut";
            return cl;
        };
        
        var loaded = false;
        function load() {
            if (loaded) return false;
            loaded = true;
            
            clear();
            
            // Watch
            watcher.on("delete", function(e) {
                removeSingleNode(e);
            });
            
            watcher.on("change.all", function(e) {
                onstat({ path: e.path, result: [null, e.stat]});
            });
            
            watcher.on("directory.all", function(e) {
                // @todo make onreaddir incremental
                onreaddir({ path: e.path, result: [null, e.files]});
            });
            
            // Read
            fs.on("beforeReaddir", function (e) {
                var node = findNode(e.path);
                if (!node) 
                    return; // Parent is not visible
                
                // Indicate this directory is being read
                model.setAttribute(node, "status", "loading");
            });
            
            function onreaddir(e, orphan) {
                var node = findNode(e.path);
                if (e.error) {
                    if (node)
                        node.status = "pending";
                    return;
                }
                var parentPath = e.path;
                if (!parentPath.endsWith("/"))
                    parentPath += "/";
                // update cache
                if (!node) {
                    if (!showHidden && isFileHidden(e.path))
                        return;
                    node = createNode(e.path);
                    if (!node) return;
                    node.isFolder = true;
                    orphans[e.path] = node;
                    orphan = true;
                }
                node.$lastReadT = Date.now();

                // Indicate this directory has been fully read
                model.setAttribute(node, "status", "loaded");
                
                var ondisk = Object.create(null);
                var toRemove = [];
                var toCreate = [];
                var orphanAppand = [];
                var existing = node.map || (node.map = Object.create(null));
                e.result[1].forEach(function(stat) {
                    if (!stat.name || !showHidden && isFileHidden(stat.name))
                        return;
                    var name = stat.name;
                    var path = parentPath + name;

                    ondisk[name] = 1;
                    
                    if (orphans[path]) {
                        if (existing[name])
                            delete orphans[path];
                        orphanAppand.push(path);
                    }
                    
                    if (existing[name])
                        updateNodeStat(path, stat, existing[name]);
                    else
                        toCreate.push(stat);
                });
                
                Object.keys(existing).forEach(function(name) {
                    // onreaddir can be called before copied nodes are written to disk
                    // in this case we don't want to lose "predicted" state
                    if (existing[name] && existing[name].status === "predicted")
                        ondisk[name] = 1;
                    if (!ondisk[name])
                        toRemove.push(name);
                });
                
                if (!toCreate.length && !toRemove.length && !orphanAppand.length) {
                    // emit readdir event for empty folders, since other plugins may use that
                    // to display create new file buttons
                    if (node.children && node.children.length)
                        return;
                }
                
                var wasOpen = startUpdate(node);
                node.children = null;
                
                // Fill Parent
                toCreate.forEach(function(stat) {
                    createNode(parentPath + stat.name, stat, null, true);
                });
                
                toRemove.forEach(function(name) {
                    var currentNode = existing[name];
                    delete existing[name];
                    emit("remove", {
                        path: parentPath + name,
                        node: currentNode,
                        parent: node
                    });
                });
                
                emit("readdir", { path: e.path, parent: node, orphan: orphan });
                
                endUpdate(node, wasOpen);
                
                orphanAppand.forEach(function(path) {
                    emit("orphan-append", { path: path });
                });
            }
            fs.on("afterReaddir", onreaddir, plugin);
            
            function onstat(e) {
                if (e.error) return;
                
                // update cache
                var stat = e.result[1];
                
                var there = true;
                var node = findNode(e.path);
                var parent = findNode(dirname(e.path));
                
                if (!showHidden && isFileHidden(e.path))
                    return;

                if (!node) { 
                    if (!parent) 
                        return;
                    there = false;
                }
                
                if (there != !!stat) {
                    if (there) {
                        if (!node.link)
                            deleteNode(node);
                    }
                    else {
                        if (typeof stat != "object")
                            stat = null;
                        createNode(e.path, stat);
                    }
                }
                else if (there) {
                    if (typeof stat != "object")
                        stat = null;
                    if (!stat && node)
                        return;
                    if (stat && node)
                        updateNodeStat(e.path, stat, node);
                    else
                        createNode(e.path, stat, node);
                }
            }
            fs.on("afterStat", onstat, plugin);
            fs.on("afterReadFile", function(e) {
                var err = e.result[0];
                return onstat({
                    path: e.path, 
                    result: [0, err && err.code == "ENOENT" 
                        ? false : true]
                });
            }, plugin);
            fs.on("afterExists", onstat, plugin);
            
            // Modify
            
            function confirmStatus(node) {
                if (node.status === "predicted")
                    node.status = node.isFolder ? "pending" : "loaded";
            }
            
            function afterHandler(e) {
                if (e.error) e.undo && e.undo();
                else e.confirm && e.confirm();
            }
            
            function addSingleNode(e, isFolder, linkInfo) {
                var node = findNode(e.path);
                if (node) return; // Node already exists
                
                if (!showHidden && isFileHidden(e.path))
                    return;
                
                var parent = findNode(dirname(e.path));
                if (parent) { // Dir is in cache
                    var stat = isFolder 
                        ? { mime: "folder" } 
                        : (linkInfo
                            ? { link: true, linkStat: { fullPath: linkInfo }}
                            : {});
                    stat.mtime = Date.now();
                    node = createNode(e.path, stat);

                    emit("add", { path: e.path, node: node });
                    
                    e.undo = function() {
                        deleteNode(node);
                        
                        emit("remove", {
                            path: e.path,
                            node: node,
                            parent: parent
                        });
                    };
                    e.confirm = function () {
                        confirmStatus(node);
                    };
                    node.status = "predicted";
                }
            }
            
            function removeSingleNode(e) {
                var node = findNode(e.path);
                if (!node) return; // Node doesn't exist
                
                deleteNode(node);
                emit("remove", {
                    path: node.path,
                    node: node
                });
                
                e.undo = function() {
                    if (this.error && this.error.code == "ENOENT") 
                        return;
                    createNode(node.path, null, node);
                    emit("add", {
                        path: node.path,
                        node: node
                    });
                };
            }
            
            function recurPathUpdate(node, oldPath, newPath, isCopy) {
                if (!isCopy) model.setAttribute(node, "oldpath", oldPath);
                model.setAttribute(node, "path", newPath);
                
                if (node.children)
                    node.children = null;
                var cnode, nodes = model.getChildren(node) || [];
                for (var i = nodes.length; i--;) {
                    cnode = nodes[i];
                    if (cnode) {
                        recurPathUpdate(cnode, cnode.path,
                            newPath + "/" + cnode.label, isCopy);
                    }
                }
                
                emit(isCopy ? "add" : "update", {
                    path: oldPath,
                    newPath: newPath,
                    node: node
                });
            }
            
            fs.on("beforeWriteFile", function(e) { addSingleNode(e); }, plugin);
            fs.on("afterWriteFile", afterHandler, plugin);
            
            // Also does move
            fs.on("beforeRename", function(e) {
                var node = findNode(e.path);
                if (!node) return;
                
                var oldPath = e.path;
                var newPath = e.args[1];
                var parent = findNode(dirname(newPath));
                
                // Validation
                var toNode = findNode(newPath);
                
                if (!toNode) {
                    deleteNode(node, true);
                    createNode(newPath, null, node); // Move node
                    recurPathUpdate(node, oldPath, newPath);
                }
                
                e.undo = function() {
                    if (toNode)
                        return;
                    if (!parent) {
                        var tmpParent = node;
                        while (node.parent && tmpParent.parent.status == "pending")
                            tmpParent = tmpParent.parent;
                        if (tmpParent)
                            deleteNode(tmpParent, true);
                    }
                    deleteNode(node, true);
                    if (toNode)
                        createNode(newPath, null, toNode);
                    
                    createNode(oldPath, null, node);
                    recurPathUpdate(node, newPath, oldPath);
                };
                e.confirm = function() {
                    if (toNode) {
                        deleteNode(toNode, true);
                        createNode(newPath, null, node); // Move node
                        recurPathUpdate(node, oldPath, newPath);
                    }
                    confirmStatus(node);
                };
                node.status = "predicted";
            }, plugin);
            fs.on("afterRename", afterHandler, plugin);
            
            fs.on("beforeMkdirP", function(e) {
                var dirsToMake = [];
                var path = e.path;
                
                while (!findNode(path)) {
                    dirsToMake.push(path);
                    path = dirname(path);
                    if (path == "~") return; // Don't create home based paths if ~ doesn't exist
                    if (path == ".") break;
                }
                
                dirsToMake.forEach(function(dir) {
                    createNode(dir, { mime: "folder" });
                });
                
                var node = dirsToMake[0] && findNode(dirsToMake[0]);
                if (!node)
                    return;
                
                e.undo = function() {
                    dirsToMake.forEach(function(dir) {
                        var node = findNode(dir);
                        if (node)
                            deleteNode(node);
                    });
                };
                e.confirm = function() {
                    confirmStatus(node);
                };
                node.status = "predicted";
            }, plugin);
            fs.on("afterMkdirP", afterHandler, plugin);
            
            fs.on("beforeMkdir", function(e) { addSingleNode(e, true); }, plugin);
            fs.on("afterMkdir", afterHandler, plugin);
            
            fs.on("beforeUnlink", function(e) { removeSingleNode(e); }, plugin);
            fs.on("afterUnlink", afterHandler, plugin);
            
            fs.on("beforeRmfile", function(e) { removeSingleNode(e); }, plugin);
            fs.on("afterRmfile", afterHandler, plugin);
            
            fs.on("beforeRmdir", function(e) { removeSingleNode(e); }, plugin);
            fs.on("afterRmdir", afterHandler, plugin);
            
            fs.on("beforeCopy", function(e) {
                var node = findNode(e.path);
                if (!node) return;
                
                var parent = findNode(dirname(e.args[1]));
                if (!parent) return;
                
                function setCacheCopy(to) {
                    toNode = copyNode(node);
                    createNode(to, null, toNode);
                    
                    //Validation
                    //var path = toNode.path;
                    //if (path != to) return;
                    
                    recurPathUpdate(toNode, e.path, to, true);
                    
                    e.undo = function() {
                        removeSingleNode({ path: to });
                    };
                    e.confirm = function() {
                        confirmStatus(node);
                    };
                    toNode.status = "predicted";
                }
                
                var toNode = findNode(e.args[1]);
                if (toNode) {
                    if (!e.args[2] || e.args[2].overwrite !== false) { //overwrite
                        deleteNode(toNode);
                    }
                    else {
                        e.setCacheCopy = setCacheCopy;
                        return; //We'll wait to get the new name from the server
                    }
                }
                
                setCacheCopy(e.args[1]);
            }, plugin);
            fs.on("afterCopy", function(e) {
                if (!e.error && e.setCacheCopy)
                    e.setCacheCopy(e.result[1].to);
                afterHandler(e);
            }, plugin);
            
            fs.on("beforeSymlink", function(e) {
                addSingleNode(e, false, e.args[1]);
            }, plugin);
            fs.on("afterSymlink", function(e) {
                if (e.error) {
                    if (e.undo)
                        e.undo();
                }
                else {
                    var node = findNode(e.path);
                    if (!node) return;
                        
                    fs.stat(e.args[1], function(err, stat) {
                        var linkNode = findNode(e.path);
                        
                        if (err || stat.err) {
                            model.setAttribute(linkNode, "error", err.message);
                            return;
                        }
                        
                        stat.fullPath = e.args[1];
                        createNode(e.path, {
                            link: true, 
                            linkStat: stat }, linkNode);
                        
                        emit("update", {
                            path: e.path,
                            node: linkNode
                        });
                    });
                }
            }, plugin);
        }
        
        /***** Methods *****/
        
        function findNode(path, context) {
            if (typeof context == "string") { // || path && path.charAt(0) != "/"
                node = emit("findNode", { type: context, path: path });
                if (node || node === false) return node;
                context = null;
            }
            
            var parts = path.split("/");
            var node = context || model.root;
            if (!node) {
                node = orphans[parts[0]]; // model.realRoot || 
                if (node) parts.shift();
            }
            
            if (path == "/") parts.shift();
            
            var up = 0;
            for (var i = parts.length; i--;) {
                var p = parts[i];
                if (!p && i || p === ".") {
                    parts.splice(i, 1);
                }
                else if (p === "..") {
                    parts.splice(i, 1);
                    up++;
                }
                else if (up) {
                    parts.splice(i, 1);
                    up--;
                }
            }
            
            for (var i = 0; i < parts.length; i++) { 
                var p = parts[i];
                if (node)
                    node = node.map && node.map[p];
                if (!node)
                    node = orphans[parts.slice(0, i + 1).join("/")];
            }
            
            return node;
        }
        
        function findNodes(path) {
            // todo ??
        }
        
        function createNode(path, stat, updateNode, updating) {
            if (!/^[!~/]/.test(path)) {
                var e = new Error("Refusing to add a node with misformed path to fs.cache");
                return reportError(e, { path: path });
            }
            
            if (orphans[path]) {
                updateNode = orphans[path];
                delete orphans[path];
            }
            
            var parts = path.split("/");
            var node = model.root.map[parts[0] == "~" ? "~" : ""];
            if (!node) {
                node = orphans[parts[0]];
                if (node) parts.shift();
            }
            
            var parent = model.root;
            var modified = [];
            var subPath = "";
            parts.forEach(function(p, i) {
                if (!p) return;
                subPath += (p == "~" ? p : "/" + p);
                
                if (!node)
                    return node = orphans[subPath];
                
                var map = node.map;
                if (!map) {
                    map = node.map = Object.create(null);
                }
                parent = node;
                node = map[p];
                if (!node) {
                    modified.push(parent);
                    if (i !== parts.length - 1) {
                        node = { label: p, path: subPath, status: "pending", isFolder: true };
                        // TODO filter hidden files in getChildren instead.
                        if (!showHidden && isFileHidden(p)) {
                            orphans[node.path] = node;
                            return;
                        }
                    } else if (updateNode) {
                        deleteNode(updateNode, true);
                        node = updateNode;
                        if (node.label != p) {
                            if (map[node.label] == node)
                                delete map[node.label];
                            node.label = p;
                        }
                    } else {
                        node = { label: p, path: subPath };
                    }
                    node.parent = parent;
                    map[p] = node;
                }
            });
            if (!node) {
                node = { label: parts[parts.length - 1], path: path };
                orphans[path] = node;
            }
            
            updateNodeStat(path, stat, node); 
            
            node.children = null;
            
            if (!updating) {
                if (!modified.length)
                    modified.push(parent);
                var wasOpen = startUpdate(modified[0]);
                modified.forEach(function(n) {
                    if (n != model.root)
                        n.children = null;
                });
                endUpdate(modified[0], wasOpen);
            }
            model._signal("createNode", node);
            return node;
        }
        
        function updateNodeStat(path, stat, node) {
            node.path = path;
            var original_stat;
            if (stat && stat.link) {
                original_stat = stat;
                stat = stat.linkStat;
            }
            if (stat) {
                var isFolder = stat && /(directory|folder)$/.test(stat.mime);
                if (isFolder) {
                    node.status = node.status || "pending";
                } else {
                    node.status = "loaded";
                }
                if (typeof stat.mtime !== "number" && stat.mtime) {
                    // TODO fix localfs to not send date objects here
                    stat.mtime = +stat.mtime;
                }
                if (stat.size != undefined)
                    node.size = stat.size;
                if (stat.mtime != undefined)
                    node.mtime = stat.mtime;
                if (original_stat || stat.linkErr)
                    node.link = stat.fullPath || stat.linkErr;
                if (isFolder)
                    node.isFolder = isFolder;
                else
                    delete node.isFolder;
            }
            
            if (node.isFolder && !node.map) {
                node.map = Object.create(null);
                node.children = null;
            } else if (!node.isFolder && node.map) {
                delete node.map;
                node.children = null;
            }
        }
        
        function deleteNode(node, silent) {
            if (findNode(node.path) != node)
                return;
            var parent = findNode(dirname(node.path));
            if (!parent) {
                // likely a broken node
                var e = new Error("Node with misformed path in fs.cache");
                reportError(e, {
                    path: node.path,
                    hasParentProp: !!node.parent,
                    parentPath: node.parent && node.parent.path
                });
                return;
            }
            if (orphans[node.path] == node) {
                delete orphans[node.path];
                if (parent.map[node.label] != node)
                    return;
            }
            silent || model._signal("remove", node);
            var wasOpen = startUpdate(parent);
            delete parent.map[node.label];
            if (parent.children)
                node.index = parent.children.indexOf(node);
            parent.children = node.children = null;
            endUpdate(parent, wasOpen);
        }
        
        function copyNode(node, parent) {
            var copy = {};
            Object.keys(node).forEach(function(key) {
                var prop;
                if (key == "parent") {
                    prop = parent;
                } else if (key == "map") {
                    prop = {};
                    Object.keys(node.map).forEach(function(label) {
                        prop[label] = copyNode(node.map[label], node);
                    });
                } else if (key === "children" || key === "isSelected") {
                    prop = null;
                } else {
                    prop = lang.deepCopy(node[key]);
                }
                copy[key] = prop;
            });
            return copy;
        }
        
        function clear() {
            var all = model.visibleItems;
            for (var i = all.length; i--;) {
                if (model.isOpen(all[i]))
                    model.collapse(all[i]);
            }
            model.projectDir = {
                label: options.rootLabel || c9.projectName,
                isFolder: true,
                path: "/",
                status: "pending",
                className: "projectRoot",
                isEditable: false,
                map: Object.create(null)
            };
            var root = {};
            root.map = Object.create(null);
            root.map[""] = model.projectDir;
            model.setRoot(root);
            // fs.readdir("/", function(){});
        }
        
        function loadNodes(path, progress) {
            var loaded = 0, subPath = "/";
            var nodes = path.split("/"), node;
            function recur() {
                node = findNode(subPath, "expand");
                if (!node) {
                    progress({
                        nodes: nodes, node: nodes[loaded - 1], 
                        loaded: loaded, complete: true
                    });
                } else if (node.status == "loaded") {
                    if (subPath.slice(-1) !== "/")
                        subPath += "/";
                    subPath += nodes[loaded];
                    nodes[loaded++] = node;
                    progress({ nodes: nodes, node: node, loaded: loaded });
                    recur();
                } else if (node.status == "loading") {
                    plugin.on("readdir", function listener(e) {
                        if (e.path == subPath) {
                            plugin.off("readdir", listener);
                            recur();
                        }
                    });
                } else if (node.status == "pending") {
                    model.loadChildren(node, recur);
                }
            }
            recur();
        }

        /***** Private *****/
        
        function updateHiddenFileRegexp(str) {
            var parts = (str || ".*").split(/,\s*/).map(function(x) {
                return x.trim().replace(/([\/'*+?|()\[\]{}.\^$])/g, function(p) {
                    if (p === "*")
                        return "[^\\/]*?";
                    else
                        return "\\" + p;
                });
            }).filter(Boolean);
            hiddenFileRe = new RegExp("(^|/)(" + parts.join("|") + ")/?$", "i");
        }
        
        function isFileHidden(name) {
            return hiddenFileRe.test(name);
        }
        
        function startUpdate(node) {
            var wasOpen = node.isOpen;
            if (wasOpen) model.close(node, undefined, true);
            model._signal("startUpdate", node);
            return wasOpen;
        }
        
        function endUpdate(node, wasOpen) {
            if (wasOpen) {
                model.open(node, undefined, true);
                model._signal("change", node);
            }
            model._signal("endUpdate", node, wasOpen);
        }
        
        function refresh(node) {
            model.close(node, undefined, true);
            model.open(node, undefined, true);
            model._signal("change", node);
        }
        
        /***** Lifecycle *****/
        
        plugin.on("load", function() {
            load();
        });
        plugin.on("enable", function() {
            
        });
        plugin.on("disable", function() {
            
        });
        plugin.on("unload", function() {
            loaded = false;
            showHidden = false;
            hiddenFilePattern = "";
            hiddenFileRe = /^$/;
            orphans = {};
        });
        
        /***** Register and define API *****/
        
        /**
         * Provides a model for storage and retrieval of xml nodes that represent
         * filesystem nodes. The model will contain all nodes that have been
         * accessed via the {@link fs} plugin.
         * @singleton
         **/
        plugin.freezePublicAPI({
            /**
             * Exposes the model object that stores the XML used to store the
             * cache. This property is here for backwards compatibility only
             * and will be removed in the next version.
             * @property model
             * @deprecated
             * @private
             */
            model: model,
            
            /** 
             * Specifies whether hidden files are shown. 
             * @property {Boolean} [showHidden=false]
             */
            get showHidden() { return showHidden; },
            set showHidden(value) { 
                showHidden = value; 
                emit("setShowHidden", { value: value });
            },
            
            /** 
             * Specifies pattern for hidden file name. 
             * @property {String}
             */
            get hiddenFilePattern() { return hiddenFilePattern; },
            set hiddenFilePattern(value) { 
                updateHiddenFileRegexp(value);
                hiddenFilePattern = value;
            },
            
            _events: [
                /** 
                 * @event orphan-append Fires when a parent of a previously loaded dir gets loaded
                 * @param {Object} e
                 * @param {String} e.path the path of the parent that got loaded
                 */
                "orphan-append",
                /**
                 * @event readdir Fires after storing the directory contents into cache
                 * @param {Object}     e
                 * @param {String}     e.path    the path of the parent that got loaded
                 * @param {XMLElement} e.parent  the parent node that is stored in cache
                 */
                "readdir",
                /**
                 * Fires when a file system node is added to the cache.
                 * @event add
                 * @param {Object} e
                 * @param {String} e.path       The path of the file system node that is added.
                 * @param {String} [e.newPath]  The new path of the file (if the new file is created by a copy)
                 * @param {Object} e.node       Object describing the file system node.
                 */
                "add",
                /**
                 * Fires when a file system node is removed from the cache.
                 * @event remove
                 * @param {Object} e
                 * @param {String} e.path       The path of the file system node that is removed.
                 * @param {Object} e.node       Object describing the file system node.
                 * @param {Object} [e.parent]   Object describing the parent of the file system node.
                 */
                "remove",
                /**
                 * Fires when a file system node is updated in the cache.
                 * @event update
                 * @param {Object} e
                 * @param {String} e.path       The path of the file system node that is updated.
                 * @param {String} [e.newPath]  The new path of the file (if the new file is created by a copy)
                 * @param {Object} e.node       Object describing the file system node.
                 */
                "update"
            ],
            
            /**
             * Returns a new xml node representing a file, folder or symlink. Note
             * that this call will not make any changes to the actual file system.
             * It will just create a node in the cache. Generally you won't need
             * to use this method.
             * @param {String} path the path of the filesystem node
             * @param {Object} stat an object similar to that returned by the fs.stat call
             * @returns {XMLElement} the created node representing the file system node.
             */
            createNode: createNode,
            
            /**
             * @ignore
             */
            removeNode: deleteNode,
            
            /**
             * Finds an xml node based on a path
             * @param {String} path the path of the node to find
             * @returns {XMLElement} the node representing the file system node.
             */
            findNode: findNode,
            
            /**
             * Finds all xml nodes based on a path (or xpath)
             * @param {String} path the path of the node to find
             * @returns {Array} the xml nodes representing the file system nodes.
             */
            findNodes: findNodes,
            
            /**
             * Removes all the nodes from the cache
             */
            clear: clear,
            
            /**
             * Refreshes a section based on a tree node
             * @param {Object} node
             */
            refresh: refresh,
            
            /**
             * Refreshes a section based on a tree node
             * @param {String} path
             * @param {Function} progress
             * @param {Function} done
             */
            loadNodes: loadNodes,
            
            /**
             * @ignore
             */
            isFileHidden: isFileHidden
        });
        
        register(null, {
            "fs.cache": plugin
        });
    }
});
