/* You may find the license in the LICENSE file */

const EXPORTED_SYMBOLS = [
	'prealloc'
];

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
const module = Cu.import;
const Exception = Components.Exception;

const FileOutputStream = Components.Constructor('@mozilla.org/network/file-output-stream;1', 'nsIFileOutputStream', 'init');
const File = Components.Constructor('@mozilla.org/file/local;1', 'nsILocalFile', 'initWithPath');

module('resource://dta/utils.jsm');
module('resource://dta/version.jsm');
module('resource://dta/cothread.jsm');

Debug.logString("Using CoThread implementation");

// Should we use the optimized Windows implementation?
const WINDOWSIMPL = Version.OS == 'winnt';
// Size cap: Use Windows implementation (on Windows) even if run on main thread
const WINDOWSIMPL_SIZEMAX = (1 << 25); // 32MB

//Minimum size of a preallocation.
//If requested size is less then no actual pre-allocation will be performed.
const SIZE_MIN = (WINDOWSIMPL ? 30 : 2048) * 1024;

//Step size of the allocation
//Do this step wise to avoid certain "sparse files" cases
const SIZE_STEP = (1 << 23); // 8MB

/**
 * Pre-allocates a given file on disk
 * and calls given callback when done
 *
 * @param file (nsIFile) file to allocate
 * @param size (int) Size to allocate
 * @param perms (int) *nix file permissions
 * @param callback (function) Callback called once done
 * @param tp (function) Scope (this) to call the callback function in
 * @return (nsICancelable) Pre-allocation object.
 */
function prealloc(file, size, perms, callback, tp) {
	tp = tp || null;
	callback = (callback || function(){}).bind(tp);
	if (size <= SIZE_MIN || !isFinite(size)) {
		Debug.logString("pa: not preallocating");
		callback(false);
		return null;
	}
	return new WorkerJob(file, size, perms, callback);
}

function WorkerJob(file, size, perms, callback) {
	this.file = file;
	this.size = size;
	this.perms = perms;
	this.callback = callback;
	let g = this.run.bind(this);
	this.coThread = new CoThreadInterleaved((i for (i in g())), 1);
	this.coThread.run(this.finish.bind(this));
}

WorkerJob.prototype = {
	result: false,
	run: function worker_run() {
		if (WINDOWSIMPL) {
			for (let i in this._run_windows()) yield i;
		}
		else {
			for (let i in this._run_other()) yield i;
		}
	},
	finish: function() {
		delete this.coThread;
		this.callback(this.result);
	},
	_run_windows: function worker_run_windows() {
		try {
			let stream = new FileOutputStream(this.file, 0x02 | 0x08, this.perms, 0);
			let size = this.size;
			try {
				let seekable = stream.QueryInterface(Ci.nsISeekableStream);
				seekable.seek(0x02, 0);
				size -= seekable.tell();
				while (size > 0) {
					let count = Math.min(size, 1 << 26 /* 64MB */);
					size -= count;
					seekable.seek(0x01, count);
					seekable.setEOF();
					yield true;
				}
				this.result = true;
			}
			finally {
				stream.close();
			}
		}
		catch (ex) {
			Debug.log("pa: Windows implementation failed!", ex);
			for (let i in this._run_other()) yield i;
		}
	},
	_run_other: function worker_run_other() {
		try {
			let stream = new FileOutputStream(this.file, 0x02 | 0x08, this.perms, 0);
			try {
				let seekable = stream.QueryInterface(Ci.nsISeekableStream);
				seekable.seek(0x02, 0);
				let i = seekable.tell();
				if (i < this.size - 1) {
					i += SIZE_STEP;
					for (; !this.terminated && i < this.size + SIZE_STEP; i += SIZE_STEP) {
						seekable.seek(0x00, Math.min(i, this.size - 1));
						stream.write("a", 1);
						yield true;
					}
					this.result = true;
				}
			}
			finally {
				stream.close();
			}
		}
		catch (ex) {
			Debug.log("pa: Failed to run prealloc loop", ex);
		}
	},
	cancel: function() {
		this.terminated = true;
	}
};
