/* 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);