/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Tobias Koppers @sokra
*/

"use strict";

const normalize = require("./normalize");
const join = require("./join");
const MemoryFileSystemError = require("./MemoryFileSystemError");
const errors = require("errno");
const stream = require("readable-stream");

const ReadableStream = stream.Readable;
const WritableStream = stream.Writable;

function isDir(item) {
	if(typeof item !== "object") return false;
	return item[""] === true;
}

function isFile(item) {
	if(typeof item !== "object") return false;
	return !item[""];
}

function pathToArray(path) {
	path = normalize(path);
	const nix = /^\//.test(path);
	if(!nix) {
		if(!/^[A-Za-z]:/.test(path)) {
			throw new MemoryFileSystemError(errors.code.EINVAL, path);
		}
		path = path.replace(/[\\\/]+/g, "\\"); // multi slashs
		path = path.split(/[\\\/]/);
		path[0] = path[0].toUpperCase();
	} else {
		path = path.replace(/\/+/g, "/"); // multi slashs
		path = path.substr(1).split("/");
	}
	if(!path[path.length-1]) path.pop();
	return path;
}

function trueFn() { return true; }
function falseFn() { return false; }

class MemoryFileSystem {
	constructor(data) {
		this.data = data || {};
		this.join = join;
		this.pathToArray = pathToArray;
		this.normalize = normalize;
	}

	meta(_path) {
		const path = pathToArray(_path);
		let current = this.data;
		let i = 0;
		for(; i < path.length - 1; i++) {
			if(!isDir(current[path[i]]))
				return;
			current = current[path[i]];
		}
		return current[path[i]];
	}

	existsSync(_path) {
		return !!this.meta(_path);
	}

	statSync(_path) {
		let current = this.meta(_path);
		if(_path === "/" || isDir(current)) {
			return {
				isFile: falseFn,
				isDirectory: trueFn,
				isBlockDevice: falseFn,
				isCharacterDevice: falseFn,
				isSymbolicLink: falseFn,
				isFIFO: falseFn,
				isSocket: falseFn
			};
		} else if(isFile(current)) {
			return {
				isFile: trueFn,
				isDirectory: falseFn,
				isBlockDevice: falseFn,
				isCharacterDevice: falseFn,
				isSymbolicLink: falseFn,
				isFIFO: falseFn,
				isSocket: falseFn
			};
		} else {
			throw new MemoryFileSystemError(errors.code.ENOENT, _path, "stat");
		}
	}

	readFileSync(_path, optionsOrEncoding) {
		const path = pathToArray(_path);
		let current = this.data;
		let i = 0
		for(; i < path.length - 1; i++) {
			if(!isDir(current[path[i]]))
				throw new MemoryFileSystemError(errors.code.ENOENT, _path, "readFile");
			current = current[path[i]];
		}
		if(!isFile(current[path[i]])) {
			if(isDir(current[path[i]]))
				throw new MemoryFileSystemError(errors.code.EISDIR, _path, "readFile");
			else
				throw new MemoryFileSystemError(errors.code.ENOENT, _path, "readFile");
		}
		current = current[path[i]];
		const encoding = typeof optionsOrEncoding === "object" ? optionsOrEncoding.encoding : optionsOrEncoding;
		return encoding ? current.toString(encoding) : current;
	}

	readdirSync(_path) {
		if(_path === "/") return Object.keys(this.data).filter(Boolean);
		const path = pathToArray(_path);
		let current = this.data;
		let i = 0;
		for(; i < path.length - 1; i++) {
			if(!isDir(current[path[i]]))
				throw new MemoryFileSystemError(errors.code.ENOENT, _path, "readdir");
			current = current[path[i]];
		}
		if(!isDir(current[path[i]])) {
			if(isFile(current[path[i]]))
				throw new MemoryFileSystemError(errors.code.ENOTDIR, _path, "readdir");
			else
				throw new MemoryFileSystemError(errors.code.ENOENT, _path, "readdir");
		}
		return Object.keys(current[path[i]]).filter(Boolean);
	}

	mkdirpSync(_path) {
		const path = pathToArray(_path);
		if(path.length === 0) return;
		let current = this.data;
		for(let i = 0; i < path.length; i++) {
			if(isFile(current[path[i]]))
				throw new MemoryFileSystemError(errors.code.ENOTDIR, _path, "mkdirp");
			else if(!isDir(current[path[i]]))
				current[path[i]] = {"":true};
			current = current[path[i]];
		}
		return;
	}

	mkdirSync(_path) {
		const path = pathToArray(_path);
		if(path.length === 0) return;
		let current = this.data;
		let i = 0;
		for(; i < path.length - 1; i++) {
			if(!isDir(current[path[i]]))
				throw new MemoryFileSystemError(errors.code.ENOENT, _path, "mkdir");
			current = current[path[i]];
		}
		if(isDir(current[path[i]]))
			throw new MemoryFileSystemError(errors.code.EEXIST, _path, "mkdir");
		else if(isFile(current[path[i]]))
			throw new MemoryFileSystemError(errors.code.ENOTDIR, _path, "mkdir");
		current[path[i]] = {"":true};
		return;
	}

	_remove(_path, name, testFn) {
		const path = pathToArray(_path);
		const operation = name === "File" ? "unlink" : "rmdir";
		if(path.length === 0) {
			throw new MemoryFileSystemError(errors.code.EPERM, _path, operation);
		}
		let current = this.data;
		let i = 0;
		for(; i < path.length - 1; i++) {
			if(!isDir(current[path[i]]))
				throw new MemoryFileSystemError(errors.code.ENOENT, _path, operation);
			current = current[path[i]];
		}
		if(!testFn(current[path[i]]))
			throw new MemoryFileSystemError(errors.code.ENOENT, _path, operation);
		delete current[path[i]];
		return;
	}

	rmdirSync(_path) {
		return this._remove(_path, "Directory", isDir);
	}

	unlinkSync(_path) {
		return this._remove(_path, "File", isFile);
	}

	readlinkSync(_path) {
		throw new MemoryFileSystemError(errors.code.ENOSYS, _path, "readlink");
	}

	writeFileSync(_path, content, optionsOrEncoding) {
		if(!content && !optionsOrEncoding) throw new Error("No content");
		const path = pathToArray(_path);
		if(path.length === 0) {
			throw new MemoryFileSystemError(errors.code.EISDIR, _path, "writeFile");
		}
		let current = this.data;
		let i = 0
		for(; i < path.length - 1; i++) {
			if(!isDir(current[path[i]]))
				throw new MemoryFileSystemError(errors.code.ENOENT, _path, "writeFile");
			current = current[path[i]];
		}
		if(isDir(current[path[i]]))
			throw new MemoryFileSystemError(errors.code.EISDIR, _path, "writeFile");
		const encoding = typeof optionsOrEncoding === "object" ? optionsOrEncoding.encoding : optionsOrEncoding;
		current[path[i]] = optionsOrEncoding || typeof content === "string" ? new Buffer.from(content, encoding) : content;
		return;
	}

	// stream methods
	createReadStream(path, options) {
		let stream = new ReadableStream();
		let done = false;
		let data;
		try {
			data = this.readFileSync(path);
		} catch (e) {
			stream._read = function() {
				if (done) {
					return;
				}
				done = true;
				this.emit('error', e);
				this.push(null);
			};
			return stream;
		}
		options = options || { };
		options.start = options.start || 0;
		options.end = options.end || data.length;
		stream._read = function() {
			if (done) {
				return;
			}
			done = true;
			this.push(data.slice(options.start, options.end));
			this.push(null);
		};
		return stream;
	}

	createWriteStream(path) {
		let stream = new WritableStream();
		try {
			// Zero the file and make sure it is writable
			this.writeFileSync(path, new Buffer.alloc(0));
		} catch(e) {
			// This or setImmediate?
			stream.once('prefinish', function() {
				stream.emit('error', e);
			});
			return stream;
		}
		let bl = [ ], len = 0;
		stream._write = (chunk, encoding, callback) => {
			bl.push(chunk);
			len += chunk.length;
			this.writeFile(path, Buffer.concat(bl, len), callback);
		}
		return stream;
	}

	// async functions
	exists(path, callback) {
		return callback(this.existsSync(path));
	}

	writeFile(path, content, encoding, callback) {
		if(!callback) {
			callback = encoding;
			encoding = undefined;
		}
		try {
			this.writeFileSync(path, content, encoding);
		} catch(e) {
			return callback(e);
		}
		return callback();
	}
}

// async functions

["stat", "readdir", "mkdirp", "rmdir", "unlink", "readlink"].forEach(function(fn) {
	MemoryFileSystem.prototype[fn] = function(path, callback) {
		let result;
		try {
			result = this[fn + "Sync"](path);
		} catch(e) {
			setImmediate(function() {
				callback(e);
			});

			return;
		}
		setImmediate(function() {
			callback(null, result);
		});
	};
});

["mkdir", "readFile"].forEach(function(fn) {
	MemoryFileSystem.prototype[fn] = function(path, optArg, callback) {
		if(!callback) {
			callback = optArg;
			optArg = undefined;
		}
		let result;
		try {
			result = this[fn + "Sync"](path, optArg);
		} catch(e) {
			setImmediate(function() {
				callback(e);
			});

			return;
		}
		setImmediate(function() {
			callback(null, result);
		});
	};
});

module.exports = MemoryFileSystem;
