(function(window) {
  // internal: same as jQuery.extend(true, args...)
  var extend = function() {
    var target = arguments[0],
        sources = [].slice.call(arguments, 1);
    for (var i = 0; i < sources.length; ++i) {
      var src = sources[i];
      for (key in src) {
        var val = src[key];
        target[key] = typeof val === "object"
          ? extend(typeof target[key] === "object" ? target[key] : {}, val)
          : val;
      }
    }
    return target;
  };

  var WORKER_FILE = {
    wav: "WebAudioRecorderWav.js",
    ogg: "WebAudioRecorderOgg.js",
    mp3: "WebAudioRecorderMp3.js"
  };

  // default configs
  var CONFIGS = {
    workerDir: "/",     // worker scripts dir (end with /)
    numChannels: 2,     // number of channels
    encoding: "wav",    // encoding (can be changed at runtime)

    // runtime options
    options: {
      timeLimit: 300,           // recording time limit (sec)
      encodeAfterRecord: false, // process encoding after recording
      progressInterval: 1000,   // encoding progress report interval (millisec)
      bufferSize: undefined,    // buffer size (use browser default)

      // encoding-specific options
      wav: {
        mimeType: "audio/wav"
      },
      ogg: {
        mimeType: "audio/ogg",
        quality: 0.5            // (VBR only): quality = [-0.1 .. 1]
      },
      mp3: {
        mimeType: "audio/mpeg",
        bitRate: 160            // (CBR only): bit rate = [64 .. 320]
      }
    }
  };

  // constructor
  var WebAudioRecorder = function(sourceNode, configs) {
    extend(this, CONFIGS, configs || {});
    this.context = sourceNode.context;
    if (this.context.createScriptProcessor == null)
      this.context.createScriptProcessor = this.context.createJavaScriptNode;
    this.input = this.context.createGain();
    sourceNode.connect(this.input);
    this.buffer = [];
    this.initWorker();
  };

  // instance methods
  extend(WebAudioRecorder.prototype, {
    isRecording: function() { return this.processor != null; },

    setEncoding: function(encoding) {
      if (this.isRecording())
        this.error("setEncoding: cannot set encoding during recording");
      else if (this.encoding !== encoding) {
        this.encoding = encoding;
        this.initWorker();
      }
    },

    setOptions: function(options) {
      if (this.isRecording())
        this.error("setOptions: cannot set options during recording");
      else {
        extend(this.options, options);
        this.worker.postMessage({ command: "options", options: this.options });
      }
    },

    startRecording: function() {
      if (this.isRecording())
        this.error("startRecording: previous recording is running");
      else {
        var numChannels = this.numChannels,
            buffer = this.buffer,
            worker = this.worker;
        this.processor = this.context.createScriptProcessor(
                                this.options.bufferSize,
                                this.numChannels, this.numChannels);
        this.input.connect(this.processor);
        this.processor.connect(this.context.destination);
        this.processor.onaudioprocess = function(event) {
          for (var ch = 0; ch < numChannels; ++ch)
            buffer[ch] = event.inputBuffer.getChannelData(ch);
          worker.postMessage({ command: "record", buffer: buffer });
        };
        this.worker.postMessage({
          command: "start",
          bufferSize: this.processor.bufferSize
        });
        this.startTime = Date.now();
      }
    },

    recordingTime: function() {
      return this.isRecording() ? (Date.now() - this.startTime) * 0.001 : null;
    },

    cancelRecording: function() {
      if (this.isRecording()) {
        this.input.disconnect();
        this.processor.disconnect();
        delete this.processor;
        this.worker.postMessage({ command: "cancel" });
      } else
        this.error("cancelRecording: no recording is running");
    },

    finishRecording: function() {
      if (this.isRecording()) {
        this.input.disconnect();
        this.processor.disconnect();
        delete this.processor;
        this.worker.postMessage({ command: "finish" });
      } else
        this.error("finishRecording: no recording is running");
    },

    cancelEncoding: function() {
      if (this.options.encodeAfterRecord)
        if (this.isRecording())
          this.error("cancelEncoding: recording is not finished");
        else {
          this.onEncodingCanceled(this);
          this.initWorker();
        }
      else
        this.error("cancelEncoding: invalid method call");
    },

    initWorker: function() {
      if (this.worker != null)
        this.worker.terminate();
      this.onEncoderLoading(this, this.encoding);
      this.worker = new Worker(this.workerDir + WORKER_FILE[this.encoding]);
      var _this = this;
      this.worker.onmessage = function(event) {
        var data = event.data;
        switch (data.command) {
          case "loaded":
            _this.onEncoderLoaded(_this, _this.encoding);
            break;
          case "timeout":
            _this.onTimeout(_this);
            break;
          case "progress":
            _this.onEncodingProgress(_this, data.progress);
            break;
          case "complete":
            _this.onComplete(_this, data.blob);
            break;
          case "error":
            _this.error(data.message);
        }
      };
      this.worker.postMessage({
        command: "init",
        config: {
          sampleRate: this.context.sampleRate,
          numChannels: this.numChannels
        },
        options: this.options
      });
    },

    error: function(message) {
      this.onError(this, "WebAudioRecorder.js:" + message);
    },

    // event handlers
    onEncoderLoading: function(recorder, encoding) {},
    onEncoderLoaded: function(recorder, encoding) {},
    onTimeout: function(recorder) { recorder.finishRecording(); },
    onEncodingProgress: function (recorder, progress) {},
    onEncodingCanceled: function(recorder) {},
    onComplete: function(recorder, blob) {
      recorder.onError(recorder, "WebAudioRecorder.js: You must override .onComplete event");
    },
    onError: function(recorder, message) { console.log(message); }
  });

  window.WebAudioRecorder = WebAudioRecorder;
})(window);