(function(global) {
  'use strict';

  // an invertible check on the condition.
  // if the constraint is applied, then the check is direct
  // if not applied, then the result should be reversed
  function check(constraintApplied, condition, message) {
    var good = constraintApplied ? condition : !condition;
    message = (constraintApplied ? 'with' : 'without') +
      ' constraint: should ' + (constraintApplied ? '' : 'not ') +
      message + ' = ' + (good ? 'OK' : 'waiting...');
    info(message);
    return good;
  }

  function mkElement(type) {
    // This makes an unattached element.
    // It's not rendered to save the cycles that costs on b2g emulator
    // and it gets dropped (and GC'd) when the test is done.
    var e = document.createElement(type);
    e.width = 32;
    e.height = 24;
    document.getElementById('display').appendChild(e);
    return e;
  }

  // Runs checkFunc until it reports success.
  // This is kludgy, but you have to wait for media to start flowing, and it
  // can't be any old media, it has to include real data, for which we have no
  // reliable signals to use as a trigger.
  function periodicCheck(checkFunc) {
    var resolve;
    var done = false;
    // This returns a function so that we create 10 closures in the loop, not
    // one; and so that the timers don't all start straight away
    var waitAndCheck = counter => () => {
      if (done) {
        return Promise.resolve();
      }
      return new Promise(r => setTimeout(r, 200 << counter))
        .then(() => {
          if (checkFunc()) {
            done = true;
            resolve();
          }
        });
    };

    var chain = Promise.resolve();
    for (var i = 0; i < 10; ++i) {
      chain = chain.then(waitAndCheck(i));
    }
    return new Promise(r => resolve = r);
  }

  function isSilence(audioData) {
    var silence = true;
    for (var i = 0; i < audioData.length; ++i) {
      if (audioData[i] !== 128) {
        silence = false;
      }
    }
    return silence;
  }

  function checkAudio(constraintApplied, stream) {
    var audio = mkElement('audio');
    audio.mozSrcObject = stream;
    audio.play();

    var context = new AudioContext();
    var source = context.createMediaStreamSource(stream);
    var analyser = context.createAnalyser();
    source.connect(analyser);
    analyser.connect(context.destination);

    return periodicCheck(() => {
      var sampleCount = analyser.frequencyBinCount;
      info('got some audio samples: ' + sampleCount);
      var buffer = new Uint8Array(sampleCount);
      analyser.getByteTimeDomainData(buffer);

      var silent = check(constraintApplied, isSilence(buffer),
                         'be silence for audio');
      return sampleCount > 0 && silent;
    }).then(() => {
      source.disconnect();
      analyser.disconnect();
      audio.pause();
      ok(true, 'audio is ' + (constraintApplied ? '' : 'not ') + 'silent');
    });
  }

  function checkVideo(constraintApplied, stream) {
    var video = mkElement('video');
    video.mozSrcObject = stream;
    video.play();

    return periodicCheck(() => {
      try {
        var canvas = mkElement('canvas');
        var ctx = canvas.getContext('2d');
        // Have to guard drawImage with the try as well, due to bug 879717. If
        // we get an error, this round fails, but that failure is usually just
        // transitory.
        ctx.drawImage(video, 0, 0);
        ctx.getImageData(0, 0, 1, 1);
        return check(constraintApplied, false, 'throw on getImageData for video');
      } catch (e) {
        return check(constraintApplied, e.name === 'SecurityError',
                     'get a security error: ' + e.name);
      }
    }).then(() => {
      video.pause();
      ok(true, 'video is ' + (constraintApplied ? '' : 'not ') + 'protected');
    });
  }

  global.audioIsSilence = checkAudio;
  global.videoIsBlack = checkVideo;
}(this));
