var async = require('async'),
    config = require('./config'),
    crypto = require('crypto'),
    etc = require('./etc'),
    fs = require('fs'),
    make_client = require('./make_client').make_maybe_minified,
    pathJoin = require('path').join,
    stream = require('stream'),
    tmp_file = require('tmp').file,
    util = require('util');

const PUBLIC_JS = pathJoin('www', 'js');

function HashingStream(out) {
	stream.Writable.call(this);

	this._hash = crypto.createHash('MD5');
	this._outStream = out;
}
util.inherits(HashingStream, stream.Writable);

HashingStream.prototype._write = function (chunk, encoding, cb) {
	this._hash.update(chunk);
	this._outStream.write(chunk, encoding, cb);
};

HashingStream.prototype.end = function (cb) {
	if (arguments.length > 1)
		throw new Error("TODO multi-arg HashingStream.end");
	var self = this;
	stream.Writable.prototype.end.call(this, function () {
		self._outStream.end(function () {
			if (cb)
				cb();
		});
	});
};

function end_and_move_js(stream, dir, prefix, cb) {
	stream.end(function () {
		var fnm;
		if (config.DEBUG) {
			fnm = prefix + '-debug.js';
		}
		else {
			var hash = stream._hash.digest('hex').slice(0, 10);
			fnm = prefix + '-' + hash + '.min.js';
		}
		var tmp = stream._tmpFilename;
		etc.move(tmp, pathJoin(dir, fnm), function (err) {
			if (err)
				return cb(err);
			cb(null, fnm);
		});
	});
};


function make_hashing_stream(cb) {
	// ideally the stream would be returned immediately and handle
	// this step internally...
	var opts = {dir: '.build', postfix: '.gen.js', mode: 0644};
	tmp_file(opts, function (err, tmp, fd) {
		if (err)
			return cb(err);
		var out = fs.createWriteStream(null, {fd: fd});
		out.once('error', cb);

		if (config.DEBUG) {
			out._tmpFilename = tmp;
			cb(null, out);
		}
		else {
			var stream = new HashingStream(out);
			stream._tmpFilename = tmp;
			cb(null, stream);
		}
	});
}

function build_vendor_js(cb) {
	var deps = require('./deps');
	make_hashing_stream(function (err, stream) {
		if (err)
			return cb(err);
		async.eachSeries(deps.VENDOR_DEPS, function (file, cb) {
			fs.readFile(file, function (err, buf) {
				if (err)
					return cb(err);
				stream.write(buf, cb);
			});
		}, function (err) {
			if (err)
				return cb(err);
			end_and_move_js(stream, PUBLIC_JS, 'vendor', cb);
		});
	});
}

function build_client_js(cb) {
	var deps = require('./deps');
	make_hashing_stream(function (err, stream) {
		if (err)
			return cb(err);
		make_client(deps.CLIENT_DEPS, stream, function (err) {
			if (err)
				return cb(err);
			end_and_move_js(stream, PUBLIC_JS, 'client', cb);
		});
	});
}

function build_mod_client_js(cb) {
	var deps = require('./deps');
	make_hashing_stream(function (err, stream) {
		if (err)
			return cb(err);
		make_client(deps.MOD_CLIENT_DEPS, stream, function (err) {
			if (err)
				return cb(err);
			end_and_move_js(stream, 'state', 'mod', cb);
		});
	});
}

function commit_assets(metadata, cb) {
	tmp_file({dir: '.build', postfix: '.json'}, function (err, tmp, fd) {
		if (err)
			return cb(err);
		var stream = fs.createWriteStream(null, {fd: fd});
		stream.once('error', cb);
		stream.end(JSON.stringify(metadata) + '\n', function () {
			etc.move(tmp, pathJoin('state', 'scripts.json'), cb);
		});
	});
}

function rebuild(cb) {
	etc.checked_mkdir('state', function (err) {
	etc.checked_mkdir('.build', function (err) {
		if (err) return cb(err);
		async.parallel({
			vendor: build_vendor_js,
			client: build_client_js,
			mod: build_mod_client_js,
		}, function (err, hashes) {
			if (err)
				return cb(err);
			commit_assets(hashes, cb);
		});
	});
	});
}
exports.rebuild = rebuild;

exports.refresh_deps = function () {
	delete require.cache[pathJoin(__dirname, 'deps.js')];
};

if (require.main === module) {
	rebuild(function (err) {
		if (err) throw err;
	});
}
