/*
 * Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

var Recorder = function(source){
  var bufferLen = 4096;
  var toneFreq = 1000, errorMargin = 0.05;

  var context = source.context;
  var sampleRate = context.sampleRate;
  var recBuffersL = [], recBuffersR = [], recLength = 0;
  this.node = (context.createScriptProcessor ||
              context.createJavaScriptNode).call(context, bufferLen, 2, 2);
  var detectAppend = false, autoStop = false, recordCallback;
  var recording = false;
  var freqString;

  this.node.onaudioprocess = function(e) {
    if (!recording) return;

    var length = e.inputBuffer.getChannelData(0).length;
    var tmpLeft = new Float32Array(length);
    var tmpRight = new Float32Array(length);
    tmpLeft.set(e.inputBuffer.getChannelData(0), 0);
    tmpRight.set(e.inputBuffer.getChannelData(1), 0);

    recBuffersL.push(tmpLeft);
    recBuffersR.push(tmpRight);
    recLength += length;
    var stop = false;

    if (autoStop && detectTone(getFreqList(tmpLeft)))
      stop = true;

    if (recordCallback) {
      var tmpLeft = recBuffersL[recBuffersL.length - 1].subarray(
          -FFT_SIZE-1, -1);
      var tmpRight = recBuffersR[recBuffersR.length - 1].subarray(
          -FFT_SIZE-1, -1);
      recordCallback(tmpLeft, tmpRight, sampleRate, stop);
    }
  }

  /**
   * Starts recording
   * @param {function} callback function to get current buffer
   * @param {boolean} detect append tone or not
   * @param {boolean} auto stop when detecting append tone
   */
  this.record = function(cb, detect, stop) {
    recordCallback = cb;
    detectAppend = detect;
    autoStop = stop;
    recording = true;
  }

  /**
   * Stops recording
   */
  this.stop = function() {
    recording = false;
    recBuffersL = mergeBuffers(recBuffersL, recLength);
    recBuffersR = mergeBuffers(recBuffersR, recLength);
    if (detectAppend) {
      var freqList = getFreqList(recBuffersL);
      var index = getToneIndices(freqList);
      removeAppendTone(index[0], index[1]);
      exportFreqList(freqList);
    }
  }

  /**
   * Gets frequencies list
   * @param {Float32Array} buffer
   * @return {array} frequencies list
   */
  getFreqList = function(buffer) {
    var prevPeak = 0;
    var valid = true;
    var freqList = [];
    for (i = 1; i < recLength; i++) {
      if (buffer[i] > 0.1 &&
          buffer[i] >= buffer[i - 1] && buffer[i] >= buffer[i + 1]) {
        if (valid) {
          var freq = sampleRate / (i - prevPeak);
          freqList.push([freq, prevPeak, i]);
          prevPeak = i;
          valid = false;
        }
      } else if (buffer[i] < -0.1) {
        valid = true;
      }
    }
    return freqList;
  }

  /**
   * Checks average frequency is in allowed error margin
   * @param {float} average frequency
   * @return {boolean} checked result pass or fail
   */
  checkFreq = function (average) {
    if (Math.abs(average - toneFreq) / toneFreq < errorMargin)
      return true;
    return false;
  }

  /**
   * Detects append tone while recording.
   * @param {array} frequencies list
   * @return {boolean} detected or not
   */
  detectTone = function(freqList) {
    var passCriterion = 50;
    // Initialize function static variables
    if (typeof detectTone.startDetected == 'undefined') {
      detectTone.startDetected = false;
      detectTone.canStop = false;
      detectTone.accumulateTone = 0;
    }

    var windowSize = 10, windowSum = 0, i;
    var detected = false;
    for (i = 0; i < freqList.length && i < windowSize; i++) {
      windowSum += freqList[i][0];
    }
    if (checkFreq(windowSum / Math.min(windowSize, freqList.length))) {
      detected = true;
      detectTone.accumulateTone++;
    }
    for (; i < freqList.length; i++) {
      windowSum = windowSum + freqList[i][0] - freqList[i - windowSize][0];
      if (checkFreq(windowSum / windowSize)) {
        detected = true;
        detectTone.accumulateTone++;
      }
    }
    if (detected) {
      if (detectTone.accumulateTone > passCriterion) {
        if (!detectTone.startDetected)
          detectTone.startDetected = true;
        else if (detectTone.canStop) {
          detectTone.startDetected = false;
          detectTone.canStop = false;
          detectTone.accumulateTone = 0;
          return true;
        }
      }
    } else {
      detectTone.accumulateTone = 0;
      if (detectTone.startDetected)
        detectTone.canStop = true;
    }
    return false;
  }

  /**
   * Gets start and end indices from a frquencies list except append tone
   * @param {array} frequencies list
   * @return {array} start and end indices
   */
  getToneIndices = function(freqList) {
    // find start and end indices
    var flag, j, k;
    var windowSize = 10, windowSum;
    var index = new Array(2);
    var scanRange = [[0, freqList.length, 1], [freqList.length - 1, -1, -1]];

    if (freqList.length == 0) return index;

    for (i = 0; i < 2; i++) {
      flag = false;
      windowSum = 0;
      for (j = scanRange[i][0], k = 0; k < windowSize && j != scanRange[i][1];
          j += scanRange[i][2], k++) {
        windowSum += freqList[j][0];
      }
      for (; j != scanRange[i][1]; j += scanRange[i][2]) {
        windowSum = windowSum + freqList[j][0] -
            freqList[j - scanRange[i][2] * windowSize][0];
        var avg = windowSum / windowSize;
        if (checkFreq(avg) && !flag) {
          flag = true;
        }
        if (!checkFreq(avg) && flag) {
          index[i] = freqList[j][1];
          break;
        }
      }
    }
    return index;
  }

  /**
   * Removes append tone from recorded buffer
   * @param {int} start index
   * @param {int} end index
   */
  removeAppendTone = function(start, end) {
    if (!isNaN(start) && !isNaN(end) && end > start) {
      recBuffersL = truncateBuffers(recBuffersL, recLength, start, end);
      recBuffersR = truncateBuffers(recBuffersR, recLength, start, end);
      recLength = end - start;
    }
  }

  /**
   * Exports frequency list for debugging purpose
   */
  exportFreqList = function(freqList) {
    freqString = sampleRate + '\n';
    for (var i = 0; i < freqList.length; i++) {
      freqString += freqList[i][0] + ' ' + freqList[i][1] + ' ' +
          freqList[i][2] + '\n';
    }
  }

  this.getFreq = function() {
    return freqString;
  }

  /**
   * Clears recorded buffer
   */
  this.clear = function() {
    recLength = 0;
    recBuffersL = [];
    recBuffersR = [];
  }

  /**
   * Gets recorded buffer
   */
  this.getBuffer = function() {
    var buffers = [];
    buffers.push(recBuffersL);
    buffers.push(recBuffersR);
    return buffers;
  }

  /**
   * Exports WAV format file
   * @return {blob} audio file blob
   */
  this.exportWAV = function(type) {
    type = type || 'audio/wav';
    var interleaved = interleave(recBuffersL, recBuffersR);
    var dataview = encodeWAV(interleaved);
    var audioBlob = new Blob([dataview], { type: type });
    return audioBlob;
  }

  /**
   * Truncates buffer from start index to end index
   * @param {Float32Array} audio buffer
   * @param {int} buffer length
   * @param {int} start index
   * @param {int} end index
   * @return {Float32Array} a truncated buffer
   */
  truncateBuffers = function(recBuffers, recLength, startIdx, endIdx) {
    var buffer = new Float32Array(endIdx - startIdx);
    for (var i = startIdx, j = 0; i < endIdx; i++, j++) {
      buffer[j] = recBuffers[i];
    }
    return buffer;
  }

  /**
   * Merges buffer into an array
   * @param {array} a list of Float32Array of audio buffer
   * @param {int} buffer length
   * @return {Float32Array} a merged buffer
   */
  mergeBuffers = function(recBuffers, recLength) {
    var result = new Float32Array(recLength);
    var offset = 0;
    for (var i = 0; i < recBuffers.length; i++){
      result.set(recBuffers[i], offset);
      offset += recBuffers[i].length;
    }
    return result;
  }

  /**
   * Interleaves left and right channel buffer
   * @param {Float32Array} left channel buffer
   * @param {Float32Array} right channel buffer
   * @return {Float32Array} an interleaved buffer
   */
  interleave = function(inputL, inputR) {
    var length = inputL.length + inputR.length;
    var result = new Float32Array(length);

    var index = 0,
      inputIndex = 0;

    while (index < length){
      result[index++] = inputL[inputIndex];
      result[index++] = inputR[inputIndex];
      inputIndex++;
    }
    return result;
  }

  floatTo16BitPCM = function(output, offset, input) {
    for (var i = 0; i < input.length; i++, offset+=2){
      var s = Math.max(-1, Math.min(1, input[i]));
      output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
    }
  }

  writeString = function(view, offset, string) {
    for (var i = 0; i < string.length; i++){
      view.setUint8(offset + i, string.charCodeAt(i));
    }
  }

  /**
   * Encodes audio buffer into WAV format raw data
   * @param {Float32Array} an interleaved buffer
   * @return {DataView} WAV format raw data
   */
  encodeWAV = function(samples) {
    var buffer = new ArrayBuffer(44 + samples.length * 2);
    var view = new DataView(buffer);

    /* RIFF identifier */
    writeString(view, 0, 'RIFF');
    /* file length */
    view.setUint32(4, 32 + samples.length * 2, true);
    /* RIFF type */
    writeString(view, 8, 'WAVE');
    /* format chunk identifier */
    writeString(view, 12, 'fmt ');
    /* format chunk length */
    view.setUint32(16, 16, true);
    /* sample format (raw) */
    view.setUint16(20, 1, true);
    /* channel count */
    view.setUint16(22, 2, true);
    /* sample rate */
    view.setUint32(24, sampleRate, true);
    /* byte rate (sample rate * block align) */
    view.setUint32(28, sampleRate * 4, true);
    /* block align (channel count * bytes per sample) */
    view.setUint16(32, 4, true);
    /* bits per sample */
    view.setUint16(34, 16, true);
    /* data chunk identifier */
    writeString(view, 36, 'data');
    /* data chunk length */
    view.setUint32(40, samples.length * 2, true);

    floatTo16BitPCM(view, 44, samples);

    return view;
  }

  source.connect(this.node);
  this.node.connect(context.destination);
};

window.Recorder = Recorder;