/* jshint worker:true */
(function main(global) {
	"use strict";

	if (global.zWorkerInitialized)
		throw new Error('z-worker.js should be run only once');
	global.zWorkerInitialized = true;

	addEventListener("message", function(event) {
		var message = event.data, type = message.type, sn = message.sn;
		var handler = handlers[type];
		if (handler) {
			try {
				handler(message);
			} catch (e) {
				onError(type, sn, e);
			}
		}
		//for debug
		//postMessage({type: 'echo', originalType: type, sn: sn});
	});

	var handlers = {
		importScripts: doImportScripts,
		newTask: newTask,
		append: processData,
		flush: processData,
	};

	// deflater/inflater tasks indexed by serial numbers
	var tasks = {};

	function doImportScripts(msg) {
		if (msg.scripts && msg.scripts.length > 0)
			importScripts.apply(undefined, msg.scripts);
		postMessage({type: 'importScripts'});
	}

	function newTask(msg) {
		var CodecClass = global[msg.codecClass];
		var sn = msg.sn;
		if (tasks[sn])
			throw Error('duplicated sn');
		tasks[sn] =  {
			codec: new CodecClass(msg.options),
			crcInput: msg.crcType === 'input',
			crcOutput: msg.crcType === 'output',
			crc: new Crc32(),
		};
		postMessage({type: 'newTask', sn: sn});
	}

	// performance may not be supported
	var now = global.performance ? global.performance.now.bind(global.performance) : Date.now;

	function processData(msg) {
		var sn = msg.sn, type = msg.type, input = msg.data;
		var task = tasks[sn];
		// allow creating codec on first append
		if (!task && msg.codecClass) {
			newTask(msg);
			task = tasks[sn];
		}
		var isAppend = type === 'append';
		var start = now();
		var output;
		if (isAppend) {
			try {
				output = task.codec.append(input, function onprogress(loaded) {
					postMessage({type: 'progress', sn: sn, loaded: loaded});
				});
			} catch (e) {
				delete tasks[sn];
				throw e;
			}
		} else {
			delete tasks[sn];
			output = task.codec.flush();
		}
		var codecTime = now() - start;

		start = now();
		if (input && task.crcInput)
			task.crc.append(input);
		if (output && task.crcOutput)
			task.crc.append(output);
		var crcTime = now() - start;

		var rmsg = {type: type, sn: sn, codecTime: codecTime, crcTime: crcTime};
		var transferables = [];
		if (output) {
			rmsg.data = output;
			transferables.push(output.buffer);
		}
		if (!isAppend && (task.crcInput || task.crcOutput))
			rmsg.crc = task.crc.get();
		postMessage(rmsg, transferables);
	}

	function onError(type, sn, e) {
		var msg = {
			type: type,
			sn: sn,
			error: formatError(e)
		};
		postMessage(msg);
	}

	function formatError(e) {
		return { message: e.message, stack: e.stack };
	}

	// Crc32 code copied from file zip.js
	function Crc32() {
		this.crc = -1;
	}
	Crc32.prototype.append = function append(data) {
		var crc = this.crc | 0, table = this.table;
		for (var offset = 0, len = data.length | 0; offset < len; offset++)
			crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xFF];
		this.crc = crc;
	};
	Crc32.prototype.get = function get() {
		return ~this.crc;
	};
	Crc32.prototype.table = (function() {
		var i, j, t, table = []; // Uint32Array is actually slower than []
		for (i = 0; i < 256; i++) {
			t = i;
			for (j = 0; j < 8; j++)
				if (t & 1)
					t = (t >>> 1) ^ 0xEDB88320;
				else
					t = t >>> 1;
			table[i] = t;
		}
		return table;
	})();

	// "no-op" codec
	function NOOP() {}
	global.NOOP = NOOP;
	NOOP.prototype.append = function append(bytes, onprogress) {
		return bytes;
	};
	NOOP.prototype.flush = function flush() {};
})(this);
