let VFS = function() {
    this.FILE = Symbol("FILE");
    this.DIRECTORY = Symbol("DIRECTORY");

    let _root = { toString: function() { return "/"; }};

    this.rootDirectory = function() {
        return _root;
    };

    this.reset = function() {
        _root = { toString: function() { return "/"; }};
    }

    this.fullPath = function(node) {
        if (node == _root) { return "/" };

        var n = node;
        var path = "";
        while (n.name) {
            path = "/" + n.name + path;
            n = n.parent;
        }
        return path;
    };

    this.getParentPath = function(path) {
        var parts = this.tokenizePath(path);
        var path = "";
        if (parts.length < 2) {
            if (parts.isFullpath) { return "/"; }
            else { return ""; }
        }
        for (var i=0; i < parts.length-1; i++) {
            path += "/" + parts[i];
        }
        return path;
    };

    this.getFileName = function(path) {
        var parts = this.tokenizePath(path);
        return parts[parts.length-1];
    };

    this.createFile = function(path, parent) {
        if (parent == undefined) { parent = _root; }
        let fsnode = { type: this.FILE, name: path, data: new ArrayBuffer(0), parent: parent, toString: function() { return this.name; }};
        parent[path] = fsnode;
        return fsnode;
    };

    this.createDirectory = function(path, parent) {
        if (parent == undefined) { parent = _root; }
        let fsnode = { type: this.DIRECTORY, name: path, parent: parent, toString: function() { return this.name; }};
        parent[path] = fsnode;

        return fsnode;
    };

    this.getChildren = function(parent, type) {
        if (parent == undefined) { parent = _root; }

        let nodes = [];
        for (var e in parent) {
            if (type == undefined || (parent[e] && parent[e].type == type)) {
                if ((parent[e].type == this.FILE || parent[e].type == this.DIRECTORY) && e != "parent") {
                    nodes.push(parent[e]);
                }
            }
        }
        nodes.sort();
        return nodes;
    };

    this.writeText = function(file, text) {
        file.data = appendBuffer(file.data, this.textToData(text));
    };

    this.textToData = function(text) {
        let chars = [];
        for (let i=0; i < text.length; i++) {
            chars.push(text.charCodeAt(i));
        }
        return new Uint8Array(chars).buffer;
    };

    this.readLine = function(file, offset) {
        if (offset == undefined) { offset = 0; }
        if (offset >= file.data.byteLength) {
            throw new Error("Input past end of file");
        }
        let view = new Uint8Array(file.data);
        let c = null;
        let str = "";
        while (offset < file.data.byteLength && c != "\n") {
            c = String.fromCharCode(view[offset]);
            if (c != "\n") {
                str += c;
            }
            offset++;
        }
        return str;
    };

    this.readText = function(file) {
        let offset = 0;
        let view = new Uint8Array(file.data);
        let c = null;
        let str = "";
        while (offset < file.data.byteLength) {
            c = String.fromCharCode(view[offset]);
            str += c
            offset++;
        }
        return str;
    };

    this.writeData = function(file, data, offset) {
        if (offset == undefined) { offset = 0; }

        let start = file.data.slice(0, offset);
        let end = file.data.slice(offset + data.byteLength, file.data.byteLength);
        file.data = start;

        if (start.byteLength < offset) {
            file.data = appendBuffer(file.data, new ArrayBuffer(offset - start.byteLength));
        }
        file.data = appendBuffer(file.data, data);
        file.data = appendBuffer(file.data, end);
    };

    this.readData = function(file, offset, length) {
        return file.data.slice(offset, offset + length);
    };

    this.getNode = function(path, parent) {
        let parts = this.tokenizePath(path);
        if (parts.isRoot) {
            return _root;
        }
        if (parts.isFullpath) {
            parent = _root;
        }
        let node = null;
        for (let i=0; i < parts.length; i++) {
            if (parts[i] == ".") {
                // move along, nothing to see here
            }
            else if (parts[i] == "..") {
                if (node.parent == undefined) {
                    node = _root;
                }
                else {
                    node = node.parent;
                    parent = node;
                }
            }
            else {
                node = parent[parts[i]];
                parent = node;
                if (node == undefined) {
                    return null;
                }
            }
        }
        return node;
    };

    this.getDataURL = async function(file) {
        let blob = null;
        let type = getTypeFromName(file.name);
        if (type) {
            blob = new Blob([file.data], { type: type });
        } 
        else {
            blob = new Blob([file.data]);
        }
        let dataUrl = await new Promise(r => {let a=new FileReader(); a.onload=r; a.readAsDataURL(blob)}).then(e => e.target.result);
        return dataUrl;
    };

    this.dataURLToBlob = function(dataURL) {
        // convert base64 to raw binary data held in a string
        // doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this
        var byteString = atob(dataURL.split(',')[1]);

        // separate out the mime component
        var mimeString = dataURL.split(',')[0].split(':')[1].split(';')[0]

        // write the bytes of the string to an ArrayBuffer
        var ab = new ArrayBuffer(byteString.length);

        // create a view into the buffer
        var ia = new Uint8Array(ab);

        // set the bytes of the buffer to the correct values
        for (var i = 0; i < byteString.length; i++) {
            ia[i] = byteString.charCodeAt(i);
        }

        // write the ArrayBuffer to a blob, and you're done
        var blob = new Blob([ab], {type: mimeString});
        return blob;
    };

    function getTypeFromName(filename) {
        var parts = filename.split(".");
        if (parts.length < 2) { return null; }
        var ext = parts.pop();
        return types[ext];
    }

    this.renameNode = function(node, newName) {
        // TODO: move the file if the newName includes a path
        var parent = node.parent;
        parent[node.name] = undefined;
        node.name = newName;
        parent[node.name] = node;
    };

    this.removeFile = function(file, parent) {
        if (typeof file == "string") {
            file = this.getNode(file, parent);
        }
        if (file && file.type == this.FILE) {
            let parent = file.parent;
            delete parent[file.name];
        }
        else {
            throw new Error("File not found");
        }
    };
    
    this.removeDirectory = function(directory, parent) {
        if (typeof directory == "string") {
            directory = this.getNode(directory, parent);
        }
        if (directory && directory.type == this.DIRECTORY) {
            var childNodes = this.getChildren(directory);
            if (childNodes.length == 0) {
                let parent = directory.parent;
                delete parent[directory.name];
            }
            else {
                // TODO: probably throw an exception
            }
        }
        else {
            // TODO: probably throw an exception
        }
    };

    this.tokenizePath = function(path) {
        path = path.replaceAll("\\","/");
        let parts = path.split("/");
        parts.isFullpath = false;
        if (path.indexOf("/") == 0) {
            parts.isFullpath = true;
        }
        if (parts[0].match(/[A-Z|a-z]:/)) {
            parts.shift();
            parts.isFullpath = true;
        }
        if (parts[0] == "") {
            while (parts.length > 0 && parts[0] == "") {
                parts.shift();
            }
        }
        if (parts.isFullpath && parts.length == 0) {
            parts.isRoot = true;
        }
        else {
            parts.isRoot = false;
        }
        return parts;
    };

    function appendBuffer(buffer1, buffer2) {
        let tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
        tmp.set(new Uint8Array(buffer1), 0);
        tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
        return tmp.buffer;        
    }

    const types = {
        //   File Extension   MIME Type
        'abs':           'audio/x-mpeg',
        'ai':            'application/postscript',
        'aif':           'audio/x-aiff',
        'aifc':          'audio/x-aiff',
        'aiff':          'audio/x-aiff',
        'aim':           'application/x-aim',
        'art':           'image/x-jg',
        'asf':           'video/x-ms-asf',
        'asx':           'video/x-ms-asf',
        'au':            'audio/basic',
        'avi':           'video/x-msvideo',
        'avx':           'video/x-rad-screenplay',
        'bcpio':         'application/x-bcpio',
        'bin':           'application/octet-stream',
        'bmp':           'image/bmp',
        'body':          'text/html',
        'cdf':           'application/x-cdf',
        'cer':           'application/pkix-cert',
        'class':         'application/java',
        'cpio':          'application/x-cpio',
        'csh':           'application/x-csh',
        'css':           'text/css',
        'dib':           'image/bmp',
        'doc':           'application/msword',
        'dtd':           'application/xml-dtd',
        'dv':            'video/x-dv',
        'dvi':           'application/x-dvi',
        'eot':           'application/vnd.ms-fontobject',
        'eps':           'application/postscript',
        'etx':           'text/x-setext',
        'exe':           'application/octet-stream',
        'gif':           'image/gif',
        'gtar':          'application/x-gtar',
        'gz':            'application/x-gzip',
        'hdf':           'application/x-hdf',
        'hqx':           'application/mac-binhex40',
        'htc':           'text/x-component',
        'htm':           'text/html',
        'html':          'text/html',
        'ief':           'image/ief',
        'jad':           'text/vnd.sun.j2me.app-descriptor',
        'jar':           'application/java-archive',
        'java':          'text/x-java-source',
        'jnlp':          'application/x-java-jnlp-file',
        'jpe':           'image/jpeg',
        'jpeg':          'image/jpeg',
        'jpg':           'image/jpeg',
        'js':            'application/javascript',
        'jsf':           'text/plain',
        'json':          'application/json',
        'jspf':          'text/plain',
        'kar':           'audio/midi',
        'latex':         'application/x-latex',
        'm3u':           'audio/x-mpegurl',
        'mac':           'image/x-macpaint',
        'man':           'text/troff',
        'mathml':        'application/mathml+xml',
        'me':            'text/troff',
        'mid':           'audio/midi',
        'midi':          'audio/midi',
        'mif':           'application/x-mif',
        'mov':           'video/quicktime',
        'movie':         'video/x-sgi-movie',
        'mp1':           'audio/mpeg',
        'mp2':           'audio/mpeg',
        'mp3':           'audio/mpeg',
        'mp4':           'video/mp4',
        'mpa':           'audio/mpeg',
        'mpe':           'video/mpeg',
        'mpeg':          'video/mpeg',
        'mpega':         'audio/x-mpeg',
        'mpg':           'video/mpeg',
        'mpv2':          'video/mpeg2',
        'ms':            'application/x-wais-source',
        'nc':            'application/x-netcdf',
        'oda':           'application/oda',
        'odb':           'application/vnd.oasis.opendocument.database',
        'odc':           'application/vnd.oasis.opendocument.chart',
        'odf':           'application/vnd.oasis.opendocument.formula',
        'odg':           'application/vnd.oasis.opendocument.graphics',
        'odi':           'application/vnd.oasis.opendocument.image',
        'odm':           'application/vnd.oasis.opendocument.text-master',
        'odp':           'application/vnd.oasis.opendocument.presentation',
        'ods':           'application/vnd.oasis.opendocument.spreadsheet',
        'odt':           'application/vnd.oasis.opendocument.text',
        'otg':           'application/vnd.oasis.opendocument.graphics-template',
        'oth':           'application/vnd.oasis.opendocument.text-web',
        'otp':           'application/vnd.oasis.opendocument.presentation-template',
        'ots':           'application/vnd.oasis.opendocument.spreadsheet-template',
        'ott':           'application/vnd.oasis.opendocument.text-template',
        'ogx':           'application/ogg',
        'ogv':           'video/ogg',
        'oga':           'audio/ogg',
        'ogg':           'audio/ogg',
        'otf':           'application/x-font-opentype',
        'spx':           'audio/ogg',
        'flac':          'audio/flac',
        'anx':           'application/annodex',
        'axa':           'audio/annodex',
        'axv':           'video/annodex',
        'xspf':          'application/xspf+xml',
        'pbm':           'image/x-portable-bitmap',
        'pct':           'image/pict',
        'pdf':           'application/pdf',
        'pgm':           'image/x-portable-graymap',
        'pic':           'image/pict',
        'pict':          'image/pict',
        'pls':           'audio/x-scpls',
        'png':           'image/png',
        'pnm':           'image/x-portable-anymap',
        'pnt':           'image/x-macpaint',
        'ppm':           'image/x-portable-pixmap',
        'ppt':           'application/vnd.ms-powerpoint',
        'pps':           'application/vnd.ms-powerpoint',
        'ps':            'application/postscript',
        'psd':           'image/vnd.adobe.photoshop',
        'qt':            'video/quicktime',
        'qti':           'image/x-quicktime',
        'qtif':          'image/x-quicktime',
        'ras':           'image/x-cmu-raster',
        'rdf':           'application/rdf+xml',
        'rgb':           'image/x-rgb',
        'rm':            'application/vnd.rn-realmedia',
        'roff':          'text/troff',
        'rtf':           'application/rtf',
        'rtx':           'text/richtext',
        'sfnt':          'application/font-sfnt',
        'sh':            'application/x-sh',
        'shar':          'application/x-shar',
        'sit':           'application/x-stuffit',
        'snd':           'audio/basic',
        'src':           'application/x-wais-source',
        'sv4cpio':       'application/x-sv4cpio',
        'sv4crc':        'application/x-sv4crc',
        'svg':           'image/svg+xml',
        'svgz':          'image/svg+xml',
        'swf':           'application/x-shockwave-flash',
        't':             'text/troff',
        'tar':           'application/x-tar',
        'tcl':           'application/x-tcl',
        'tex':           'application/x-tex',
        'texi':          'application/x-texinfo',
        'texinfo':       'application/x-texinfo',
        'tif':           'image/tiff',
        'tiff':          'image/tiff',
        'tr':            'text/troff',
        'tsv':           'text/tab-separated-values',
        'ttf':           'application/x-font-ttf',
        'txt':           'text/plain',
        'ulw':           'audio/basic',
        'ustar':         'application/x-ustar',
        'vxml':          'application/voicexml+xml',
        'xbm':           'image/x-xbitmap',
        'xht':           'application/xhtml+xml',
        'xhtml':         'application/xhtml+xml',
        'xls':           'application/vnd.ms-excel',
        'xml':           'application/xml',
        'xpm':           'image/x-xpixmap',
        'xsl':           'application/xml',
        'xslt':          'application/xslt+xml',
        'xul':           'application/vnd.mozilla.xul+xml',
        'xwd':           'image/x-xwindowdump',
        'vsd':           'application/vnd.visio',
        'wav':           'audio/x-wav',
        'wbmp':          'image/vnd.wap.wbmp',
        'wml':           'text/vnd.wap.wml',
        'wmlc':          'application/vnd.wap.wmlc',
        'wmls':          'text/vnd.wap.wmlsc',
        'wmlscriptc':    'application/vnd.wap.wmlscriptc',
        'wmv':           'video/x-ms-wmv',
        'woff':          'application/font-woff',
        'woff2':         'application/font-woff2',
        'wrl':           'model/vrml',
        'wspolicy':      'application/wspolicy+xml',
        'z':             'application/x-compress',
        'zip':           'application/zip'
    };
}


if (!ArrayBuffer.prototype.slice) {
    ArrayBuffer.prototype.slice = function (start, end) {
        let that = new Uint8Array(this);
        if (end == undefined) end = that.length;
        let result = new ArrayBuffer(end - start);
        let resultArray = new Uint8Array(result);
        for (let i = 0; i < resultArray.length; i++)
           resultArray[i] = that[i + start];
        return result;
    }
}


