/**
 * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
 *
 * Use of this source code is governed by a BSD-style license
 * that can be found in the LICENSE file in the root of the source
 * tree. An additional intellectual property rights grant can be found
 * in the file PATENTS.  All contributing project authors may
 * be found in the AUTHORS file in the root of the source tree.
 */

// LoopbackTest establish a one way loopback call between 2 peer connections
// while continuously monitoring bandwidth stats. The idea is to use this as
// a base for other future tests and to keep track of more than just bandwidth
// stats.
//
// Usage:
//  var test = new LoopbackTest(stream, callDurationMs,
//                              forceTurn, pcConstraints,
//                              maxVideoBitrateKbps);
//  test.run(onDone);
//  function onDone() {
//    test.getResults(); // return stats recorded during the loopback test.
//  }
//
function LoopbackTest(
    stream,
    callDurationMs,
    forceTurn,
    pcConstraints,
    maxVideoBitrateKbps) {

  var pc1StatTracker;
  var pc2StatTracker;

  // In order to study effect of network (e.g. wifi) on peer connection one can
  // establish a loopback call and force it to go via a turn server. This way
  // the call won't switch to local addresses. That is achieved by filtering out
  // all non-relay ice candidades on both peers.
  function constrainTurnCandidates(pc) {
    var origAddIceCandidate = pc.addIceCandidate;
    pc.addIceCandidate = function (candidate, successCallback,
                                   failureCallback) {
      if (forceTurn && candidate.candidate.indexOf("typ relay ") == -1) {
        trace("Dropping non-turn candidate: " + candidate.candidate);
        successCallback();
        return;
      } else {
        origAddIceCandidate.call(this, candidate, successCallback,
                                 failureCallback);
      }
    }
  }

  // FEC makes it hard to study bwe estimation since there seems to be a spike
  // when it is enabled and disabled. Disable it for now. FEC issue tracked on:
  // https://code.google.com/p/webrtc/issues/detail?id=3050
  function constrainOfferToRemoveFec(pc) {
    var origCreateOffer = pc.createOffer;
    pc.createOffer = function (successCallback, failureCallback, options) {
      function filteredSuccessCallback(desc) {
        desc.sdp = desc.sdp.replace(/(m=video 1 [^\r]+)(116 117)(\r\n)/g,
                                    '$1\r\n');
        desc.sdp = desc.sdp.replace(/a=rtpmap:116 red\/90000\r\n/g, '');
        desc.sdp = desc.sdp.replace(/a=rtpmap:117 ulpfec\/90000\r\n/g, '');
        successCallback(desc);
      }
      origCreateOffer.call(this, filteredSuccessCallback, failureCallback,
                           options);
    }
  }

  // Constraint max video bitrate by modifying the SDP when creating an answer.
  function constrainBitrateAnswer(pc) {
    var origCreateAnswer = pc.createAnswer;
    pc.createAnswer = function (successCallback, failureCallback, options) {
      function filteredSuccessCallback(desc) {
        if (maxVideoBitrateKbps) {
          desc.sdp = desc.sdp.replace(
              /a=mid:video\r\n/g,
              'a=mid:video\r\nb=AS:' + maxVideoBitrateKbps + '\r\n');
        }
        successCallback(desc);
      }
      origCreateAnswer.call(this, filteredSuccessCallback, failureCallback,
                            options);
    }
  }

  // Run the actual LoopbackTest.
  this.run = function(doneCallback) {
    if (forceTurn) requestTurn(start, fail);
    else start();

    function start(turnServer) {
      var pcConfig = forceTurn ? { iceServers: [turnServer] } : null;
      console.log(pcConfig);
      var pc1 = new RTCPeerConnection(pcConfig, pcConstraints);
      constrainTurnCandidates(pc1);
      constrainOfferToRemoveFec(pc1);
      pc1StatTracker = new StatTracker(pc1, 50);
      pc1StatTracker.recordStat("EstimatedSendBitrate",
                                "bweforvideo", "googAvailableSendBandwidth");
      pc1StatTracker.recordStat("TransmitBitrate",
                                "bweforvideo", "googTransmitBitrate");
      pc1StatTracker.recordStat("TargetEncodeBitrate",
                                "bweforvideo", "googTargetEncBitrate");
      pc1StatTracker.recordStat("ActualEncodedBitrate",
                                "bweforvideo", "googActualEncBitrate");

      var pc2 = new RTCPeerConnection(pcConfig, pcConstraints);
      constrainTurnCandidates(pc2);
      constrainBitrateAnswer(pc2);
      pc2StatTracker = new StatTracker(pc2, 50);
      pc2StatTracker.recordStat("REMB",
                                "bweforvideo", "googAvailableReceiveBandwidth");

      pc1.addStream(stream);
      var call = new Call(pc1, pc2);

      call.start();
      setTimeout(function () {
          call.stop();
          pc1StatTracker.stop();
          pc2StatTracker.stop();
          success();
        }, callDurationMs);
    }

    function success() {
      trace("Success");
      doneCallback();
    }

    function fail(msg) {
      trace("Fail: " + msg);
      doneCallback();
    }
  }

  // Returns a google visualization datatable with the recorded samples during
  // the loopback test.
  this.getResults = function () {
    return mergeDataTable(pc1StatTracker.dataTable(),
                          pc2StatTracker.dataTable());
  }

  // Helper class to establish and manage a call between 2 peer connections.
  // Usage:
  //   var c = new Call(pc1, pc2);
  //   c.start();
  //   c.stop();
  //
  function Call(pc1, pc2) {
    pc1.onicecandidate = applyIceCandidate.bind(pc2);
    pc2.onicecandidate = applyIceCandidate.bind(pc1);

    function applyIceCandidate(e) {
      if (e.candidate) {
        this.addIceCandidate(new RTCIceCandidate(e.candidate),
                             onAddIceCandidateSuccess,
                             onAddIceCandidateError);
      }
    }

    function onAddIceCandidateSuccess() {}
    function onAddIceCandidateError(error) {
      trace("Failed to add Ice Candidate: " + error.toString());
    }

    this.start = function() {
      pc1.createOffer(gotDescription1, onCreateSessionDescriptionError);

      function onCreateSessionDescriptionError(error) {
        trace('Failed to create session description: ' + error.toString());
      }

      function gotDescription1(desc){
        trace("Offer: " + desc.sdp);
        pc1.setLocalDescription(desc);
        pc2.setRemoteDescription(desc);
        // Since the "remote" side has no media stream we need
        // to pass in the right constraints in order for it to
        // accept the incoming offer of audio and video.
        pc2.createAnswer(gotDescription2, onCreateSessionDescriptionError);
      }

      function gotDescription2(desc){
        trace("Answer: " + desc.sdp);
        pc2.setLocalDescription(desc);
        pc1.setRemoteDescription(desc);
      }
    }

    this.stop = function() {
      pc1.close();
      pc2.close();
    }
  }

  // Request a turn server. This uses the same servers as apprtc.
  function requestTurn(successCallback, failureCallback) {
    var currentDomain = document.domain;
    if (currentDomain.search('localhost') === -1 &&
        currentDomain.search('webrtc.googlecode.com') === -1) {
      failureCallback("Domain not authorized for turn server: " +
                      currentDomain);
      return;
    }

    // Get a turn server from computeengineondemand.appspot.com.
    var turnUrl = 'https://computeengineondemand.appspot.com/' +
                  'turn?username=156547625762562&key=4080218913';
    var xmlhttp = new XMLHttpRequest();
    xmlhttp.onreadystatechange = onTurnResult;
    xmlhttp.open('GET', turnUrl, true);
    xmlhttp.send();

    function onTurnResult() {
      if (this.readyState !== 4) {
        return;
      }

      if (this.status === 200) {
        var turnServer = JSON.parse(xmlhttp.responseText);
        // Create turnUris using the polyfill (adapter.js).
        turnServer.uris = turnServer.uris.filter(
            function (e) { return e.search('transport=udp') != -1; }
        );
        var iceServers = createIceServers(turnServer.uris,
                                          turnServer.username,
                                          turnServer.password);
        if (iceServers !== null) {
          successCallback(iceServers);
          return;
        }
      }
      failureCallback("Failed to get a turn server.");
    }
  }
}